blob: a3baed71aa8f74f8881f1a2addcfa71f6c3b920f [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.
//! A yield that is guaranteed to suspend.
//!
//! Long CPU passes (recovery walks, artifact hashing) hand the core back to
//! the reactor by awaiting a short timer. The runtime's timer wheel registers
//! a timer only when its deadline is still in the future at the wheel's OWN
//! clock re-read, and its sleep future completes on the first poll without
//! ever suspending when registration is refused -- so a fixed short duration
//! is a race against the code path between the two clock reads. A bare 1 us
//! sleep wins that race ~99.8% of the time even on a cold debug build, but
//! the losses are silent: at the walk's ~512 refills per GiB a 2e-3 loss
//! rate skips a yield every few GiB and nothing notices, because the pass
//! still completes. No constant wins the race deterministically; the only
//! deterministic shape is to retry with a growing duration until one
//! registration wins.
use std::time::Duration;
use tracing::error;
/// First attempted timer duration. The common case: on a warm path one
/// microsecond outlives the registration window and the first attempt wins.
const YIELD_FIRST_ATTEMPT: Duration = Duration::from_micros(1);
/// Ceiling for the attempt doubling, so no single attempt parks the caller
/// for more than a second even under a wildly stalling clock.
const YIELD_ATTEMPT_CAP: Duration = Duration::from_secs(1);
/// Attempts before giving up. Reaching it takes the clock advancing by the
/// attempted duration between `sleep()`'s deadline capture and the wheel's
/// re-read, 32 consecutive times with doubling durations -- a broken timer
/// runtime, not a lost race. An unyieldable reactor must cost throughput,
/// never wedge the boot, so the loop is bounded and the terminal case
/// degrades to not yielding at all, loudly.
const YIELD_MAX_ATTEMPTS: u32 = 32;
/// Hands the core back to the reactor: a poll of a REGISTERED timer returns
/// `Pending`, so awaiting it suspends, on any machine, by construction.
///
/// A registered timer with a near-now deadline fires on the reactor's next
/// turn, so the attempted duration barely throttles the caller at the 1 us
/// first attempt (~12 us measured per yield); it does throttle roughly
/// linearly once attempts grow past ~10 us, which only a lost race causes. A
/// bare self-waking yield is no alternative: this runtime does not reliably
/// re-poll a task that wakes itself from inside its own poll, and a task
/// parked that way may never resume.
pub async fn yield_to_reactor() {
let mut attempt = YIELD_FIRST_ATTEMPT;
for _ in 0..YIELD_MAX_ATTEMPTS {
let mut timer = std::pin::pin!(compio::time::sleep(attempt));
if futures::poll!(timer.as_mut()).is_pending() {
timer.await;
return;
}
attempt = (attempt * 2).min(YIELD_ATTEMPT_CAP);
}
error!(
"no timer registration won in {YIELD_MAX_ATTEMPTS} attempts; \
continuing without yielding to the reactor"
);
}
#[cfg(test)]
mod tests {
use super::*;
use std::task::{Context, Waker};
// Pins the suspension point itself: a yield whose future is Ready on its
// first poll never hands the core back, and nothing else would notice
// (callers still finish their pass, just without ever suspending).
#[compio::test]
async fn given_yield_future_when_polled_once_should_be_pending() {
let mut future = std::pin::pin!(yield_to_reactor());
let mut context = Context::from_waker(Waker::noop());
assert!(
future.as_mut().poll(&mut context).is_pending(),
"the first poll must register a real timer instead of completing inline"
);
}
#[compio::test]
async fn given_yield_future_when_awaited_should_complete() {
yield_to_reactor().await;
}
}