krabka_client_consumer/
control.rs1use std::sync::{
5 Arc,
6 atomic::{AtomicBool, Ordering},
7};
8
9use krabka_units::{Time, convert::TimeExt, secs};
10
11use crate::{
12 consumer::{Consumer, GroupMembershipOperation},
13 error::ConsumerError,
14};
15
16pub const DEFAULT_CONSUMER_CLOSE_TIMEOUT: Time = secs(30);
18
19pub(crate) const ENFORCED_REBALANCE_REASON: &str = "rebalance enforced by user";
22
23#[derive(Clone, Copy, Debug, Default, PartialEq, bon::Builder)]
26pub struct CloseOptions {
27 #[builder(default)]
29 pub group_membership_operation: GroupMembershipOperation,
30 #[builder(with = |millis: i64| Time::from_millis(millis))]
33 pub timeout: Option<Time>,
34}
35
36impl CloseOptions {
37 #[must_use]
39 pub fn timeout(timeout: Time) -> Self {
40 Self {
41 timeout: Some(timeout),
42 ..Self::default()
43 }
44 }
45
46 #[must_use]
49 pub fn group_membership_operation(operation: GroupMembershipOperation) -> Self {
50 Self::builder()
51 .group_membership_operation(operation)
52 .build()
53 }
54}
55
56#[derive(Clone, Copy, Debug, Default)]
58pub(crate) struct CloseRequest {
59 pub operation: GroupMembershipOperation,
60}
61
62pub(crate) fn close_timeout(options: CloseOptions) -> Result<Time, ConsumerError> {
64 let timeout = options.timeout.unwrap_or(DEFAULT_CONSUMER_CLOSE_TIMEOUT);
65 if timeout.secs_f64() < 0.0 || !timeout.secs_f64().is_finite() {
66 return Err(ConsumerError::InvalidArgument(
67 "The timeout cannot be negative.".to_owned(),
68 ));
69 }
70 Ok(timeout)
71}
72
73#[derive(Clone, Debug, Default)]
80pub struct WakeupHandle {
81 inner: Arc<WakeupState>,
82}
83
84#[derive(Debug, Default)]
85struct WakeupState {
86 pending: AtomicBool,
87 notify: tokio::sync::Notify,
88}
89
90impl WakeupHandle {
91 pub fn wakeup(&self) {
93 self.inner.pending.store(true, Ordering::SeqCst);
94 self.inner.notify.notify_waiters();
95 }
96
97 pub(crate) fn take(&self) -> bool {
99 self.inner.pending.swap(false, Ordering::SeqCst)
100 }
101
102 pub(crate) async fn woken(&self) {
104 loop {
105 let notified = self.inner.notify.notified();
106 tokio::pin!(notified);
107 notified.as_mut().enable();
108 if self.take() {
109 return;
110 }
111 notified.await;
112 }
113 }
114}
115
116impl Consumer {
117 #[must_use]
121 pub fn wakeup_handle(&self) -> WakeupHandle {
122 self.wakeup.clone()
123 }
124
125 pub fn wakeup(&self) {
128 self.wakeup.wakeup();
129 }
130
131 pub fn enforce_rebalance(&self, reason: Option<&str>) -> Result<(), ConsumerError> {
144 self.require_group_membership()?;
145 if self.group_protocol == crate::GroupProtocol::Consumer {
146 tracing::warn!("enforce_rebalance is not supported in the consumer group protocol");
150 return Ok(());
151 }
152 if self.subscription.borrow().manual_assignment {
153 return Err(ConsumerError::IllegalState(
154 "Tried to force a rebalance but the consumer has a manual assignment.".to_owned(),
155 ));
156 }
157 let reason = reason
158 .filter(|reason| !reason.is_empty())
159 .unwrap_or(ENFORCED_REBALANCE_REASON);
160 let polls = *self.poll_signal.borrow();
166 let _ = self.enforced_rebalances.send((reason.to_owned(), polls));
167 Ok(())
168 }
169}
170
171#[cfg(test)]
172mod tests {
173 use krabka_units::millis;
174
175 use super::*;
176
177 #[test]
178 fn close_timeout_follows_kafkas_close() {
179 let cases = [
180 (
181 "default timeout",
182 CloseOptions::builder().build(),
183 Ok(DEFAULT_CONSUMER_CLOSE_TIMEOUT),
184 ),
185 (
186 "finite timeout",
187 CloseOptions::timeout(millis(250)),
188 Ok(millis(250)),
189 ),
190 (
191 "negative timeout",
192 CloseOptions::timeout(Time::from_secs_f64(-1.0)),
193 Err("invalid argument: The timeout cannot be negative.".to_owned()),
194 ),
195 (
196 "NaN timeout",
197 CloseOptions::timeout(Time::from_secs_f64(f64::NAN)),
198 Err("invalid argument: The timeout cannot be negative.".to_owned()),
199 ),
200 (
201 "infinite timeout",
202 CloseOptions::timeout(Time::from_secs_f64(f64::INFINITY)),
203 Err("invalid argument: The timeout cannot be negative.".to_owned()),
204 ),
205 ];
206
207 for (name, options, expected) in cases {
208 let actual = close_timeout(options).map_err(|error| error.to_string());
209 assert2::assert!(actual == expected, "case: {name}");
210 }
211 }
212
213 #[tokio::test]
214 async fn wakeup_is_taken_once() {
215 let handle = WakeupHandle::default();
216 handle.wakeup();
217 let first =
218 tokio::time::timeout(std::time::Duration::from_millis(50), handle.woken()).await;
219 let second =
220 tokio::time::timeout(std::time::Duration::from_millis(50), handle.woken()).await;
221 assert2::assert!((first.is_ok(), second.is_ok()) == (true, false));
222 }
223}