use std::sync::{
Arc,
atomic::{AtomicBool, Ordering},
};
use krabka_units::{Time, convert::TimeExt, secs};
use crate::{
consumer::{Consumer, GroupMembershipOperation},
error::ConsumerError,
};
pub const DEFAULT_CONSUMER_CLOSE_TIMEOUT: Time = secs(30);
pub(crate) const ENFORCED_REBALANCE_REASON: &str = "rebalance enforced by user";
#[derive(Clone, Copy, Debug, Default, PartialEq, bon::Builder)]
pub struct CloseOptions {
#[builder(default)]
pub group_membership_operation: GroupMembershipOperation,
#[builder(with = |millis: i64| Time::from_millis(millis))]
pub timeout: Option<Time>,
}
impl CloseOptions {
#[must_use]
pub fn timeout(timeout: Time) -> Self {
Self {
timeout: Some(timeout),
..Self::default()
}
}
#[must_use]
pub fn group_membership_operation(operation: GroupMembershipOperation) -> Self {
Self::builder()
.group_membership_operation(operation)
.build()
}
}
#[derive(Clone, Copy, Debug, Default)]
pub(crate) struct CloseRequest {
pub operation: GroupMembershipOperation,
}
pub(crate) fn close_timeout(options: CloseOptions) -> Result<Time, ConsumerError> {
let timeout = options.timeout.unwrap_or(DEFAULT_CONSUMER_CLOSE_TIMEOUT);
if timeout.secs_f64() < 0.0 || !timeout.secs_f64().is_finite() {
return Err(ConsumerError::InvalidArgument(
"The timeout cannot be negative.".to_owned(),
));
}
Ok(timeout)
}
#[derive(Clone, Debug, Default)]
pub struct WakeupHandle {
inner: Arc<WakeupState>,
}
#[derive(Debug, Default)]
struct WakeupState {
pending: AtomicBool,
notify: tokio::sync::Notify,
}
impl WakeupHandle {
pub fn wakeup(&self) {
self.inner.pending.store(true, Ordering::SeqCst);
self.inner.notify.notify_waiters();
}
pub(crate) fn take(&self) -> bool {
self.inner.pending.swap(false, Ordering::SeqCst)
}
pub(crate) async fn woken(&self) {
loop {
let notified = self.inner.notify.notified();
tokio::pin!(notified);
notified.as_mut().enable();
if self.take() {
return;
}
notified.await;
}
}
}
impl Consumer {
#[must_use]
pub fn wakeup_handle(&self) -> WakeupHandle {
self.wakeup.clone()
}
pub fn wakeup(&self) {
self.wakeup.wakeup();
}
pub fn enforce_rebalance(&self, reason: Option<&str>) -> Result<(), ConsumerError> {
self.require_group_membership()?;
if self.group_protocol == crate::GroupProtocol::Consumer {
tracing::warn!("enforce_rebalance is not supported in the consumer group protocol");
return Ok(());
}
if self.subscription.borrow().manual_assignment {
return Err(ConsumerError::IllegalState(
"Tried to force a rebalance but the consumer has a manual assignment.".to_owned(),
));
}
let reason = reason
.filter(|reason| !reason.is_empty())
.unwrap_or(ENFORCED_REBALANCE_REASON);
let polls = *self.poll_signal.borrow();
let _ = self.enforced_rebalances.send((reason.to_owned(), polls));
Ok(())
}
}
#[cfg(test)]
mod tests {
use krabka_units::millis;
use super::*;
#[test]
fn close_timeout_follows_kafkas_close() {
let cases = [
(
"default timeout",
CloseOptions::builder().build(),
Ok(DEFAULT_CONSUMER_CLOSE_TIMEOUT),
),
(
"finite timeout",
CloseOptions::timeout(millis(250)),
Ok(millis(250)),
),
(
"negative timeout",
CloseOptions::timeout(Time::from_secs_f64(-1.0)),
Err("invalid argument: The timeout cannot be negative.".to_owned()),
),
(
"NaN timeout",
CloseOptions::timeout(Time::from_secs_f64(f64::NAN)),
Err("invalid argument: The timeout cannot be negative.".to_owned()),
),
(
"infinite timeout",
CloseOptions::timeout(Time::from_secs_f64(f64::INFINITY)),
Err("invalid argument: The timeout cannot be negative.".to_owned()),
),
];
for (name, options, expected) in cases {
let actual = close_timeout(options).map_err(|error| error.to_string());
assert2::assert!(actual == expected, "case: {name}");
}
}
#[tokio::test]
async fn wakeup_is_taken_once() {
let handle = WakeupHandle::default();
handle.wakeup();
let first =
tokio::time::timeout(std::time::Duration::from_millis(50), handle.woken()).await;
let second =
tokio::time::timeout(std::time::Duration::from_millis(50), handle.woken()).await;
assert2::assert!((first.is_ok(), second.is_ok()) == (true, false));
}
}