blob: 95715e042be719fc80f6f4853c618758514d54ba [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
//! Single-threaded edge-triggered notify.
//!
//! Lets a parked caller wake when `commit_min` advances. Multiple callers may
//! park concurrently (one per in-flight in-process submit); a notify wakes
//! every parked waiter, each re-checks its own gate and re-parks if still not
//! satisfied.
//!
//! Edge-triggered: a notify with no parked waiter is lost. Once a caller has
//! decided it must wait, it MUST arm-then-check: build the [`Notified`] future
//! (registering intent) BEFORE re-reading the gated state, so a notify landing
//! between that read and the await is not missed. Check the gate FIRST and only
//! arm when a wait is unavoidable - arming registers a roster slot, and the hot
//! already-satisfied path should leave the roster untouched.
//!
//! Each [`Notified`] deregisters its slot on drop (and `notify_all` drains the
//! roster), so the roster stays bounded to currently-live waiters.
use std::cell::RefCell;
use std::future::Future;
use std::pin::Pin;
use std::rc::Rc;
use std::task::{Context, Poll, Waker};
/// Slot for one parked waiter. `Rc`-shared between the [`Notified`] future and
/// the [`CommitSignal`]'s roster so a wake reaches the future's task.
#[derive(Default)]
struct Slot {
waker: RefCell<Option<Waker>>,
notified: RefCell<bool>,
}
/// Edge-triggered single-threaded notify. Not `Send`/`Sync`: state is
/// `Rc<RefCell<_>>`, thread-sharing would race at borrow.
///
/// The roster is an `Rc<RefCell<_>>` so each [`Notified`] keeps a handle to
/// deregister its own slot on drop, bounding the roster to live waiters.
#[derive(Debug, Default)]
pub struct CommitSignal {
signal: Rc<RefCell<Vec<Rc<Slot>>>>,
}
impl std::fmt::Debug for Slot {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Slot").finish_non_exhaustive()
}
}
impl CommitSignal {
/// Wake every parked waiter. Drains the roster: each waiter re-arms on its
/// next `commit_advanced()` if its gate is still unsatisfied. A drained
/// slot's [`Notified::drop`] becomes a no-op (the entry is already gone).
pub fn notify_all(&self) {
let slots = std::mem::take(&mut *self.signal.borrow_mut());
for slot in slots {
*slot.notified.borrow_mut() = true;
if let Some(waker) = slot.waker.borrow_mut().take() {
waker.wake();
}
}
}
/// Arm a single-use wait. Build this only once a wait is unavoidable and
/// re-check the gate AFTER (arm-then-check) so a notify landing in between
/// is not lost. The returned future resolves on the next `notify_all`.
#[must_use]
pub fn notified(&self) -> Notified {
let slot = Rc::new(Slot::default());
self.signal.borrow_mut().push(Rc::clone(&slot));
Notified {
slot,
signal: Rc::clone(&self.signal),
}
}
#[cfg(test)]
fn parked_count(&self) -> usize {
self.signal.borrow().len()
}
}
/// One-shot future that resolves on the next [`CommitSignal::notify_all`].
/// Dropping before resolution deregisters its slot, so the roster stays
/// bounded to currently-live waiters.
pub struct Notified {
slot: Rc<Slot>,
signal: Rc<RefCell<Vec<Rc<Slot>>>>,
}
impl Future for Notified {
type Output = ();
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
if *self.slot.notified.borrow() {
return Poll::Ready(());
}
*self.slot.waker.borrow_mut() = Some(cx.waker().clone());
Poll::Pending
}
}
impl Drop for Notified {
fn drop(&mut self) {
// Idempotent: if `notify_all` already drained the slot it is absent.
self.signal
.borrow_mut()
.retain(|parked| !Rc::ptr_eq(parked, &self.slot));
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::task::Context;
#[test]
fn notify_wakes_armed_waiter() {
let signal = CommitSignal::default();
let mut fut = signal.notified();
let waker = futures::task::noop_waker();
let mut cx = Context::from_waker(&waker);
assert!(Pin::new(&mut fut).poll(&mut cx).is_pending());
signal.notify_all();
assert!(Pin::new(&mut fut).poll(&mut cx).is_ready());
}
#[test]
fn notify_before_poll_is_not_lost() {
// Arm-then-check: future created (armed) before the notify; the notify
// must still resolve it even though it was never polled Pending first.
let signal = CommitSignal::default();
let mut fut = signal.notified();
signal.notify_all();
let waker = futures::task::noop_waker();
let mut cx = Context::from_waker(&waker);
assert!(Pin::new(&mut fut).poll(&mut cx).is_ready());
}
#[test]
fn notify_wakes_all_parked_waiters() {
let signal = CommitSignal::default();
let mut a = signal.notified();
let mut b = signal.notified();
let waker = futures::task::noop_waker();
let mut cx = Context::from_waker(&waker);
assert!(Pin::new(&mut a).poll(&mut cx).is_pending());
assert!(Pin::new(&mut b).poll(&mut cx).is_pending());
signal.notify_all();
assert!(Pin::new(&mut a).poll(&mut cx).is_ready());
assert!(Pin::new(&mut b).poll(&mut cx).is_ready());
}
#[test]
fn notify_with_no_waiter_is_dropped() {
// Edge-triggered: a notify with nothing parked is lost. A waiter armed
// afterwards stays pending (it must re-arm to catch the next edge).
let signal = CommitSignal::default();
signal.notify_all();
let mut fut = signal.notified();
let waker = futures::task::noop_waker();
let mut cx = Context::from_waker(&waker);
assert!(Pin::new(&mut fut).poll(&mut cx).is_pending());
}
#[test]
fn dropping_unpolled_notified_deregisters_slot() {
let signal = CommitSignal::default();
assert_eq!(signal.parked_count(), 0);
let armed = signal.notified();
assert_eq!(signal.parked_count(), 1, "arming registers a slot");
drop(armed);
assert_eq!(signal.parked_count(), 0, "drop deregisters the slot");
// A surviving waiter still wakes exactly once: the abandoned slot left
// no stale entry to double-wake.
let mut live = signal.notified();
let waker = futures::task::noop_waker();
let mut cx = Context::from_waker(&waker);
assert!(Pin::new(&mut live).poll(&mut cx).is_pending());
signal.notify_all();
assert!(Pin::new(&mut live).poll(&mut cx).is_ready());
}
#[test]
fn repeated_arm_drop_keeps_roster_bounded() {
// Models the hot path's transient parks: many arm-then-abandon cycles
// must not grow the roster (the leak this Drop impl prevents).
let signal = CommitSignal::default();
for _ in 0..1000 {
let armed = signal.notified();
assert_eq!(signal.parked_count(), 1);
drop(armed);
assert_eq!(signal.parked_count(), 0);
}
assert_eq!(signal.parked_count(), 0, "roster bounded to live waiters");
}
#[test]
fn notify_all_then_drop_is_idempotent_no_op() {
// A slot drained by notify_all is already gone; the eventual drop must
// not panic or remove an unrelated waiter.
let signal = CommitSignal::default();
let mut resolved = signal.notified();
let other = signal.notified();
assert_eq!(signal.parked_count(), 2);
signal.notify_all();
assert_eq!(signal.parked_count(), 0, "notify_all drains the roster");
let waker = futures::task::noop_waker();
let mut cx = Context::from_waker(&waker);
assert!(Pin::new(&mut resolved).poll(&mut cx).is_ready());
// Re-arm a fresh waiter, then drop the already-drained ones: their drop
// must not touch the fresh slot.
let fresh = signal.notified();
assert_eq!(signal.parked_count(), 1);
drop(resolved);
drop(other);
assert_eq!(signal.parked_count(), 1, "drained slots' drop is a no-op");
drop(fresh);
assert_eq!(signal.parked_count(), 0);
}
}