Skip to main content

future_queue/
future_queue_grouped.rs

1// Copyright (c) The buffer-unordered-weighted Contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4use crate::{
5    global_weight::GlobalWeight, peekable_fused::PeekableFused, slots::SlotReservations,
6    FutureQueueContext,
7};
8use debug_ignore::DebugIgnore;
9use fnv::FnvHashMap;
10use futures_util::{
11    ready,
12    stream::{Fuse, FusedStream, FuturesUnordered},
13    Future, Stream, StreamExt,
14};
15use pin_project_lite::pin_project;
16use std::{
17    borrow::Borrow,
18    collections::VecDeque,
19    fmt,
20    hash::Hash,
21    pin::Pin,
22    task::{Context, Poll},
23};
24
25pin_project! {
26    /// Stream for the [`future_queue_grouped`](crate::StreamExt::future_queue_grouped) method.
27    #[must_use = "streams do nothing unless polled"]
28    pub struct FutureQueueGrouped<St, K>
29    where
30        St: Stream,
31        St::Item: GroupedWeightedFuture,
32     {
33        #[pin]
34        stream: PeekableFused<Fuse<St>>,
35        #[pin]
36        in_progress_queue: PeekableFused<InProgressQueue<St>>,
37        global_weight: GlobalWeight,
38        slots: SlotReservations,
39        group_store: GroupStore<<St::Item as GroupedWeightedFuture>::Q, K, <St::Item as GroupedWeightedFuture>::F>,
40        extra_verify: bool,
41    }
42}
43
44type InProgressQueue<St> = FuturesUnordered<
45    FutureWithGW<
46        <<St as Stream>::Item as GroupedWeightedFuture>::Future,
47        <<St as Stream>::Item as GroupedWeightedFuture>::Q,
48    >,
49>;
50
51impl<St, K> fmt::Debug for FutureQueueGrouped<St, K>
52where
53    St: Stream + fmt::Debug,
54    St::Item: GroupedWeightedFuture,
55    <St::Item as GroupedWeightedFuture>::Future: fmt::Debug,
56    <<St::Item as GroupedWeightedFuture>::Future as Future>::Output: fmt::Debug,
57    K: fmt::Debug,
58    <St::Item as GroupedWeightedFuture>::Q: fmt::Debug,
59{
60    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61        f.debug_struct("FutureQueueGrouped")
62            .field("stream", &self.stream)
63            .field("in_progress_queue", &self.in_progress_queue)
64            .field("global_weight", &self.global_weight)
65            .field("slots", &self.slots)
66            .field("group_store", &self.group_store)
67            .field("extra_verify", &self.extra_verify)
68            .finish()
69    }
70}
71
72impl<St, K> FutureQueueGrouped<St, K>
73where
74    St: Stream,
75    St::Item: GroupedWeightedFuture,
76    <St::Item as GroupedWeightedFuture>::Q: Eq + Hash + fmt::Debug,
77    K: Eq + Hash + fmt::Debug + Borrow<<St::Item as GroupedWeightedFuture>::Q>,
78{
79    pub(super) fn new(
80        stream: St,
81        max_global_weight: usize,
82        id_data: impl IntoIterator<Item = (K, usize)>,
83    ) -> Self {
84        let id_data_store = GroupStore::new(id_data);
85        Self {
86            stream: PeekableFused::new(stream.fuse()),
87            in_progress_queue: PeekableFused::new(FuturesUnordered::new()),
88            global_weight: GlobalWeight::new(max_global_weight),
89            slots: SlotReservations::with_capacity(max_global_weight),
90            group_store: id_data_store,
91            extra_verify: false,
92        }
93    }
94
95    /// Sets a mode where extra internal verifications are performed.
96    #[doc(hidden)]
97    pub fn set_extra_verify(&mut self, verify: bool) {
98        self.extra_verify = verify;
99        self.slots.set_check_reserved(verify);
100        for data in self.group_store.group_data.values_mut() {
101            data.slots.set_check_reserved(verify);
102        }
103    }
104
105    /// Returns the maximum weight of futures allowed to be run by this adaptor.
106    pub fn max_global_weight(&self) -> usize {
107        self.global_weight.max()
108    }
109
110    /// Returns the current global weight of futures.
111    pub fn current_global_weight(&self) -> usize {
112        self.global_weight.current()
113    }
114
115    /// Returns the maximum weight of futures allowed to be run within this group.
116    pub fn max_group_weight<Q>(&self, id: &Q) -> Option<usize>
117    where
118        Q: Eq + Hash + fmt::Debug + ?Sized,
119        K: Borrow<Q>,
120    {
121        self.group_store
122            .group_data
123            .get(id)
124            .map(|id_data| id_data.max_weight)
125    }
126
127    /// Returns the current weight of futures being run within this group.
128    pub fn current_group_weight<Q>(&self, id: &Q) -> Option<usize>
129    where
130        Q: Eq + Hash + fmt::Debug + ?Sized,
131        K: Borrow<Q>,
132    {
133        self.group_store
134            .group_data
135            .get(id)
136            .map(|id_data| id_data.current_weight)
137    }
138
139    /// Acquires a reference to the underlying sink or stream that this combinator is
140    /// pulling from.
141    pub fn get_ref(&self) -> &St {
142        self.stream.get_ref().get_ref()
143    }
144
145    /// Acquires a mutable reference to the underlying sink or stream that this
146    /// combinator is pulling from.
147    ///
148    /// Note that care must be taken to avoid tampering with the state of the
149    /// sink or stream which may otherwise confuse this combinator.
150    pub fn get_mut(&mut self) -> &mut St {
151        self.stream.get_mut().get_mut()
152    }
153
154    /// Acquires a pinned mutable reference to the underlying sink or stream that this
155    /// combinator is pulling from.
156    ///
157    /// Note that care must be taken to avoid tampering with the state of the
158    /// sink or stream which may otherwise confuse this combinator.
159    pub fn get_pin_mut(self: Pin<&mut Self>) -> core::pin::Pin<&mut St> {
160        self.project().stream.get_pin_mut().get_pin_mut()
161    }
162
163    /// Consumes this combinator, returning the underlying sink or stream.
164    ///
165    /// Note that this may discard intermediate state of this combinator, so
166    /// care should be taken to avoid losing resources when this is called.
167    pub fn into_inner(self) -> St {
168        self.stream.into_inner().into_inner()
169    }
170
171    // ---
172    // Helper methods
173    // ---
174
175    /// Verifies that there are no queue heads that can be started.
176    fn verify_no_startable_queue_heads(&self) {
177        for (id, data) in &self.group_store.group_data {
178            if let Some(&(weight, _, _)) = data.queued.front() {
179                assert!(
180                    !data.can_start(weight, &self.global_weight),
181                    "for group {id:?}, the queued future at the head (weight {weight}) fits \
182                     within both the global weight ({}/{}) and the group weight ({}/{}), \
183                     but was not started",
184                    self.global_weight.current(),
185                    self.global_weight.max(),
186                    data.current_weight,
187                    data.max_weight,
188                );
189            }
190        }
191    }
192
193    // This returns true if any new futures were added to the in_progress_queue.
194    #[allow(clippy::type_complexity)]
195    fn poll_pop_in_progress(
196        self: Pin<&mut Self>,
197        cx: &mut Context<'_>,
198    ) -> Poll<(
199        Option<<<St::Item as GroupedWeightedFuture>::Future as Future>::Output>,
200        bool,
201    )> {
202        let mut this = self.project();
203
204        match ready!(this.in_progress_queue.poll_next_unpin(cx)) {
205            Some((weight, global_slot, id_and_group_slot, output)) => {
206                this.global_weight.sub_weight(weight);
207                this.slots.release(global_slot);
208
209                let mut any_queued = false;
210                let in_progress_queue = this.in_progress_queue.get_ref();
211                let mut start_queued = |data: &mut GroupData<_, _>| {
212                    start_queued_futures(data, this.global_weight, this.slots, in_progress_queue)
213                };
214
215                if let Some((id, group_slot)) = id_and_group_slot {
216                    let data = this.group_store.get_id_mut_or_unwrap(&id);
217                    data.sub_weight(&id, weight);
218                    data.slots.release(group_slot);
219
220                    // Try starting a future from this group first, prioritizing
221                    // the group.
222                    //
223                    // XXX: This behavior is left over from before the fix for
224                    // #603. Maybe we don't necessarily want to prioritize this
225                    // group and be globally FIFO instead? Worth thinking
226                    // carefully about this in the future.
227                    any_queued |= start_queued(data);
228                }
229
230                // This can unblock any group's queue head (#603).
231                for data in this.group_store.group_data.values_mut() {
232                    any_queued |= start_queued(data);
233                }
234
235                Poll::Ready((Some(output), any_queued))
236            }
237            None => Poll::Ready((None, false)),
238        }
239    }
240}
241
242impl<St, K> FutureQueueGrouped<St, K>
243where
244    St: Stream,
245    St::Item: GroupedWeightedFuture,
246    <St::Item as GroupedWeightedFuture>::Q: Eq + Hash + fmt::Debug,
247    K: Eq + Hash + fmt::Debug + Borrow<<St::Item as GroupedWeightedFuture>::Q>,
248{
249    fn poll_next_impl(
250        mut self: Pin<&mut Self>,
251        cx: &mut Context<'_>,
252    ) -> Poll<Option<<<St::Item as GroupedWeightedFuture>::Future as Future>::Output>> {
253        // First, attempt to pull the next value from the in progress queue.
254        // Don't return early on Pending, since the stream must be polled to
255        // register its waker.
256        let (return_output, mut any_queued) = match self.as_mut().poll_pop_in_progress(cx) {
257            Poll::Ready(ret) => ret,
258            Poll::Pending => (None, false),
259        };
260
261        let mut this = self.as_mut().project();
262
263        // Next, let's try to spawn off as many futures as possible by filling up our queue of
264        // futures.
265
266        while let Poll::Ready(Some(weighted_future)) = this.stream.as_mut().poll_peek(cx) {
267            let weight = weighted_future.weight();
268            if !this.global_weight.has_space_for(weight) {
269                // Global limits would be exceeded, break out of the loop. Consider this
270                // item next time.
271                break;
272            }
273            // We *do not* care about the group limit before pulling this item out. That's because
274            // if the group is full, it will be queued up in the group queue.
275
276            // Grab the next element from the queue.
277            let (weight, id, future_fn) = match this.stream.as_mut().poll_next(cx) {
278                Poll::Ready(Some(weighted_future)) => weighted_future.into_components(),
279                _ => unreachable!("we just peeked at this item"),
280            };
281
282            if let Some(id) = id {
283                // Is this group full?
284                let data = this.group_store.get_id_mut_or_unwrap(&id);
285                if data.has_space_for(weight) {
286                    start_grouped_future(
287                        weight,
288                        id,
289                        future_fn,
290                        data,
291                        this.global_weight,
292                        this.slots,
293                        this.in_progress_queue.get_ref(),
294                    );
295                    any_queued = true;
296                } else {
297                    data.queued.push_back((weight, id, DebugIgnore(future_fn)));
298                }
299            } else {
300                // No ID associated with this future.
301                this.global_weight.add_weight(weight);
302
303                let global_slot = this.slots.reserve();
304                let cx = FutureQueueContext {
305                    global_slot,
306                    group_slot: None,
307                };
308                let future = future_fn(cx);
309
310                this.in_progress_queue.get_ref().push(FutureWithGW::new(
311                    weight,
312                    global_slot,
313                    None,
314                    future,
315                ));
316                any_queued = true;
317            }
318        }
319
320        if any_queued {
321            // Start any futures that were just queued up. If this returns Pending, then that's fine --
322            // the task will be scheduled on the waker.
323            let _ = this.in_progress_queue.as_mut().poll_peek(cx);
324        }
325
326        if let Some(output) = return_output {
327            // A value was returned from the in-progress queue.
328            Poll::Ready(Some(output))
329        } else {
330            match (
331                self.stream.is_done(),
332                self.in_progress_queue.is_terminated(),
333            ) {
334                (true, true) => {
335                    // No more futures left to schedule. (Note that poll_pop_in_progress would have
336                    // drained all futures in any queue.)
337                    debug_assert_eq!(
338                        self.group_store.num_queued_futures(),
339                        0,
340                        "no futures should be left in any group queue"
341                    );
342                    Poll::Ready(None)
343                }
344                (false, true) => {
345                    // The in-progress queue is empty, but the stream is still pending.
346                    // (Note that Poll::Pending is OK to return here because this can only happen in
347                    // the Poll::Pending case above.)
348                    Poll::Pending
349                }
350                (_, false) => {
351                    // There are still futures in the in-progress queue. We need to poll the
352                    // in-progress queue to start any futures in it.
353                    let (output, any_queued) = ready!(self.as_mut().poll_pop_in_progress(cx));
354                    if any_queued {
355                        // It's possible that poll_pop_in_progress might have added more futures to the queue.
356                        let this = self.project();
357                        let _ = this.in_progress_queue.poll_peek(cx);
358                    }
359                    Poll::Ready(output)
360                }
361            }
362        }
363    }
364}
365
366impl<St, K> Stream for FutureQueueGrouped<St, K>
367where
368    St: Stream,
369    St::Item: GroupedWeightedFuture,
370    <St::Item as GroupedWeightedFuture>::Q: Eq + Hash + fmt::Debug,
371    K: Eq + Hash + fmt::Debug + Borrow<<St::Item as GroupedWeightedFuture>::Q>,
372{
373    type Item = <<St::Item as GroupedWeightedFuture>::Future as Future>::Output;
374
375    fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
376        let ret = self.as_mut().poll_next_impl(cx);
377        if self.extra_verify {
378            self.verify_no_startable_queue_heads();
379        }
380        ret
381    }
382
383    fn size_hint(&self) -> (usize, Option<usize>) {
384        // The minimum size is the in progress queue + any queued futures.
385        let queue_len =
386            self.in_progress_queue.size_hint().0 + self.group_store.num_queued_futures();
387        let (lower, upper) = self.stream.size_hint();
388        let lower = lower.saturating_add(queue_len);
389        let upper = match upper {
390            Some(x) => x.checked_add(queue_len),
391            None => None,
392        };
393        (lower, upper)
394    }
395}
396
397/// Attempts to start queued futures.
398///
399/// Returns true if any futures were started.
400fn start_queued_futures<Q, F, Fut>(
401    data: &mut GroupData<Q, F>,
402    global_weight: &mut GlobalWeight,
403    slots: &mut SlotReservations,
404    in_progress_queue: &FuturesUnordered<FutureWithGW<Fut, Q>>,
405) -> bool
406where
407    Q: fmt::Debug,
408    F: FnOnce(FutureQueueContext) -> Fut,
409{
410    let mut any_started = false;
411    while let Some(&(weight, _, _)) = data.queued.front() {
412        if !data.can_start(weight, global_weight) {
413            break;
414        }
415
416        let (weight, id, future_fn) = data
417            .queued
418            .pop_front()
419            .expect("front() returned Some, so pop_front() returns Some");
420        start_grouped_future(
421            weight,
422            id,
423            future_fn.0,
424            data,
425            global_weight,
426            slots,
427            in_progress_queue,
428        );
429        any_started = true;
430    }
431    any_started
432}
433
434/// Starts a grouped future, adding it to the in-progress queue.
435///
436/// The caller must have checked that there is space in both the global and
437/// group weights for this future. (See [`GroupData::can_start`].)
438fn start_grouped_future<Q, F, Fut>(
439    weight: usize,
440    id: Q,
441    future_fn: F,
442    data: &mut GroupData<Q, F>,
443    global_weight: &mut GlobalWeight,
444    slots: &mut SlotReservations,
445    in_progress_queue: &FuturesUnordered<FutureWithGW<Fut, Q>>,
446) where
447    Q: fmt::Debug,
448    F: FnOnce(FutureQueueContext) -> Fut,
449{
450    global_weight.add_weight(weight);
451    data.add_weight(&id, weight);
452
453    let global_slot = slots.reserve();
454    let group_slot = data.slots.reserve();
455
456    let cx = FutureQueueContext {
457        global_slot,
458        group_slot: Some(group_slot),
459    };
460    let future = future_fn(cx);
461
462    in_progress_queue.push(FutureWithGW::new(
463        weight,
464        global_slot,
465        Some((id, group_slot)),
466        future,
467    ));
468}
469
470struct GroupStore<Q, K, F> {
471    group_data: FnvHashMap<K, GroupData<Q, F>>,
472}
473
474impl<Q: fmt::Debug, K: fmt::Debug, F> fmt::Debug for GroupStore<Q, K, F> {
475    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
476        f.debug_struct("GroupStore")
477            .field("group_data", &self.group_data)
478            .finish()
479    }
480}
481
482impl<Q, K, F> GroupStore<Q, K, F>
483where
484    Q: Hash + Eq + fmt::Debug,
485    K: Eq + Hash + fmt::Debug + Borrow<Q>,
486{
487    fn new(ids: impl IntoIterator<Item = (K, usize)>) -> Self {
488        let id_data = ids
489            .into_iter()
490            .map(|(id, weight)| {
491                let data = GroupData {
492                    current_weight: 0,
493                    max_weight: weight,
494                    slots: SlotReservations::with_capacity(weight),
495                    queued: VecDeque::new(),
496                };
497                (id, data)
498            })
499            .collect();
500
501        Self {
502            group_data: id_data,
503        }
504    }
505
506    fn get_id_mut_or_unwrap(&mut self, id: &Q) -> &mut GroupData<Q, F> {
507        if self.group_data.contains_key(id) {
508            // Can't just use get_mut above because we're going to run into
509            // https://doc.rust-lang.org/nomicon/lifetime-mismatch.html#improperly-reduced-borrows
510            // with the else branch.
511            self.group_data.get_mut(id).unwrap()
512        } else {
513            panic!(
514                "unknown semaphore ID: {:?} (known IDs: {:?})",
515                id,
516                self.group_data.keys()
517            );
518        }
519    }
520
521    fn num_queued_futures(&self) -> usize {
522        self.group_data.values().map(|data| data.queued.len()).sum()
523    }
524}
525
526struct GroupData<Q, F> {
527    current_weight: usize,
528    max_weight: usize,
529    slots: SlotReservations,
530    queued: VecDeque<(usize, Q, DebugIgnore<F>)>,
531}
532
533impl<Q: fmt::Debug, F> fmt::Debug for GroupData<Q, F> {
534    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
535        f.debug_struct("GroupData")
536            .field("current_weight", &self.current_weight)
537            .field("max_weight", &self.max_weight)
538            .field("slots", &self.slots)
539            .field("queued", &self.queued)
540            .finish()
541    }
542}
543
544impl<Q: fmt::Debug, Fut> GroupData<Q, Fut> {
545    fn has_space_for(&self, weight: usize) -> bool {
546        let weight = weight.min(self.max_weight);
547        self.current_weight <= self.max_weight - weight
548    }
549
550    fn can_start(&self, weight: usize, global_weight: &GlobalWeight) -> bool {
551        global_weight.has_space_for(weight) && self.has_space_for(weight)
552    }
553
554    // The ID is passed in only for its Debug impl.
555    fn add_weight(&mut self, id: &Q, weight: usize) {
556        let weight = weight.min(self.max_weight);
557        self.current_weight = self.current_weight.checked_add(weight).unwrap_or_else(|| {
558            panic!(
559                "future_queue_grouped: for id `{:?}`, added weight {} to current {}, overflowed",
560                id, weight, self.current_weight,
561            )
562        });
563    }
564
565    fn sub_weight(&mut self, id: &Q, weight: usize) {
566        let weight = weight.min(self.max_weight);
567        self.current_weight = self.current_weight.checked_sub(weight).unwrap_or_else(|| {
568            panic!(
569                "future_queue_grouped: for id `{:?}`, sub weight {} from current {}, underflowed",
570                id, weight, self.current_weight,
571            )
572        });
573    }
574}
575
576pin_project! {
577    #[must_use = "futures do nothing unless polled"]
578    struct FutureWithGW<Fut, Q> {
579        #[pin]
580        future: Fut,
581        weight: usize,
582        global_slot: u64,
583        // The second parameter is the group slot.
584        id_and_group_slot: Option<(Q, u64)>,
585    }
586}
587
588impl<Fut, Q> FutureWithGW<Fut, Q> {
589    pub fn new(
590        weight: usize,
591        global_slot: u64,
592        id_and_group_slot: Option<(Q, u64)>,
593        future: Fut,
594    ) -> Self {
595        Self {
596            future,
597            weight,
598            global_slot,
599            id_and_group_slot,
600        }
601    }
602}
603
604impl<Fut, Q> Future for FutureWithGW<Fut, Q>
605where
606    Fut: Future,
607{
608    type Output = (usize, u64, Option<(Q, u64)>, Fut::Output);
609    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
610        let this = self.project();
611
612        match this.future.poll(cx) {
613            Poll::Pending => Poll::Pending,
614            Poll::Ready(output) => Poll::Ready((
615                *this.weight,
616                *this.global_slot,
617                this.id_and_group_slot.take(),
618                output,
619            )),
620        }
621    }
622}
623
624/// A trait for types which can be converted into functions that return a
625/// `Future`, an optional group, and a weight.
626///
627/// Provided in case it's necessary. This trait is only implemented for `(usize, Option<Q>, impl Future)`.
628pub trait GroupedWeightedFuture: private::Sealed {
629    /// The function to obtain the future from.
630    type F: FnOnce(FutureQueueContext) -> Self::Future;
631
632    /// The associated `Future` type.
633    type Future: Future;
634
635    /// The associated key lookup type.
636    type Q;
637
638    /// Returns the weight.
639    fn weight(&self) -> usize;
640
641    /// Turns self into its components.
642    fn into_components(self) -> (usize, Option<Self::Q>, Self::F);
643}
644
645impl<F, Fut, Q> private::Sealed for (usize, Option<Q>, F)
646where
647    F: FnOnce(FutureQueueContext) -> Fut,
648    Fut: Future,
649{
650}
651
652impl<F, Fut, Q> GroupedWeightedFuture for (usize, Option<Q>, F)
653where
654    F: FnOnce(FutureQueueContext) -> Fut,
655    Fut: Future,
656{
657    type F = F;
658    type Future = Fut;
659    type Q = Q;
660
661    #[inline]
662    fn weight(&self) -> usize {
663        self.0
664    }
665
666    #[inline]
667    fn into_components(self) -> (usize, Option<Self::Q>, Self::F) {
668        self
669    }
670}
671
672pub(crate) mod private {
673    pub trait Sealed {}
674}