1use crate::client::Client;
4use crate::config::{ClientConfig, SecurityProtocol};
5use crate::error::{BrokerErrorKind, Error, Result};
6use crate::metrics::ClientMetrics;
7use kafrust_protocol::api::api_versions::ApiVersionsResponseV3;
8use kafrust_protocol::api::find_coordinator::FindCoordinatorResponseV1;
9pub use kafrust_protocol::api::streams_group_heartbeat::{
10 StreamsGroupHeartbeatEndpoint, StreamsGroupHeartbeatEndpointPartitions,
11 StreamsGroupHeartbeatKeyValue, StreamsGroupHeartbeatRequestV0, StreamsGroupHeartbeatResponseV0,
12 StreamsGroupHeartbeatStatus, StreamsGroupHeartbeatSubtopology, StreamsGroupHeartbeatTask,
13 StreamsGroupHeartbeatTaskOffset, StreamsGroupHeartbeatTopic, StreamsGroupHeartbeatTopicConfig,
14 StreamsGroupHeartbeatTopology,
15};
16use std::collections::{BTreeMap, BTreeSet};
17use std::time::Duration;
18use tokio::sync::{mpsc, oneshot, watch};
19use tokio::task::JoinHandle;
20use tokio::time::sleep;
21use tracing::debug;
22
23const STREAMS_GROUP_HEARTBEAT_API_KEY: i16 = 88;
24const DEFAULT_STREAMS_GROUP_MAX_RETRIES: u32 = 5;
25const STREAMS_GROUP_RETRY_BACKOFF: Duration = Duration::from_millis(50);
26const STREAMS_GROUP_MAX_RETRY_BACKOFF: Duration = Duration::from_secs(1);
27const STREAMS_GROUP_COMMAND_CAPACITY: usize = 16;
28
29pub struct StreamsGroupConfig {
35 client: ClientConfig,
36 group_id: String,
37 topology: StreamsGroupHeartbeatTopology,
38 instance_id: Option<String>,
39 rack_id: Option<String>,
40 rebalance_timeout_ms: i32,
41 process_id: Option<String>,
42 user_endpoint: Option<StreamsGroupHeartbeatEndpoint>,
43 client_tags: Option<Vec<StreamsGroupHeartbeatKeyValue>>,
44 max_retries: u32,
45}
46
47impl StreamsGroupConfig {
48 pub fn new(
50 bootstrap_servers: impl IntoIterator<Item = impl Into<String>>,
51 group_id: impl Into<String>,
52 topology: StreamsGroupHeartbeatTopology,
53 ) -> Self {
54 Self {
55 client: ClientConfig::new(bootstrap_servers),
56 group_id: group_id.into(),
57 topology,
58 instance_id: None,
59 rack_id: None,
60 rebalance_timeout_ms: 30_000,
61 process_id: None,
62 user_endpoint: None,
63 client_tags: None,
64 max_retries: DEFAULT_STREAMS_GROUP_MAX_RETRIES,
65 }
66 }
67
68 pub fn client_config(mut self, client: ClientConfig) -> Self {
73 self.client = client;
74 self
75 }
76
77 pub fn with_client_config(self, client: ClientConfig) -> Self {
80 self.client_config(client)
81 }
82
83 pub fn client_id(mut self, client_id: impl Into<String>) -> Self {
85 self.client = self.client.client_id(client_id);
86 self
87 }
88
89 pub fn instance_id(mut self, instance_id: impl Into<String>) -> Self {
91 self.instance_id = Some(instance_id.into());
92 self
93 }
94
95 pub fn rack_id(mut self, rack_id: impl Into<String>) -> Self {
97 self.rack_id = Some(rack_id.into());
98 self
99 }
100
101 pub fn rebalance_timeout_ms(mut self, timeout_ms: i32) -> Self {
103 self.rebalance_timeout_ms = timeout_ms;
104 self
105 }
106
107 pub fn process_id(mut self, process_id: impl Into<String>) -> Self {
109 self.process_id = Some(process_id.into());
110 self
111 }
112
113 pub fn user_endpoint(mut self, endpoint: StreamsGroupHeartbeatEndpoint) -> Self {
115 self.user_endpoint = Some(endpoint);
116 self
117 }
118
119 pub fn client_tags(mut self, tags: Vec<StreamsGroupHeartbeatKeyValue>) -> Self {
121 self.client_tags = Some(tags);
122 self
123 }
124
125 pub fn max_retries(mut self, max_retries: u32) -> Self {
127 self.max_retries = max_retries;
128 self
129 }
130
131 pub fn request_timeout_ms(mut self, timeout_ms: u64) -> Self {
133 self.client = self.client.request_timeout_ms(timeout_ms);
134 self
135 }
136
137 pub fn security_protocol(mut self, security_protocol: SecurityProtocol) -> Self {
139 self.client = self.client.security_protocol(security_protocol);
140 self
141 }
142
143 pub fn tls_server_name(mut self, server_name: impl Into<String>) -> Self {
145 self.client = self.client.tls_server_name(server_name);
146 self
147 }
148
149 pub fn tls_root_certificate_der(mut self, certificate: impl Into<Vec<u8>>) -> Self {
151 self.client = self.client.tls_root_certificate_der(certificate);
152 self
153 }
154
155 pub fn tls_client_certificate_der(mut self, certificate: impl Into<Vec<u8>>) -> Self {
157 self.client = self.client.tls_client_certificate_der(certificate);
158 self
159 }
160
161 pub fn tls_client_private_key_der(mut self, key: impl Into<Vec<u8>>) -> Self {
163 self.client = self.client.tls_client_private_key_der(key);
164 self
165 }
166
167 pub fn metrics(mut self, metrics: ClientMetrics) -> Self {
169 self.client = self.client.metrics(metrics);
170 self
171 }
172
173 pub fn group_id(&self) -> &str {
175 &self.group_id
176 }
177
178 pub fn topology(&self) -> &StreamsGroupHeartbeatTopology {
180 &self.topology
181 }
182
183 pub fn validate(&self) -> Result<()> {
185 self.client.validate()?;
186 if self.group_id.trim().is_empty() {
187 return Err(Error::InvalidConfiguration {
188 field: "group_id",
189 reason: "must not be empty",
190 });
191 }
192 if self.topology.subtopologies.is_empty() {
193 return Err(Error::InvalidConfiguration {
194 field: "topology.subtopologies",
195 reason: "must contain at least one subtopology",
196 });
197 }
198 if self.rebalance_timeout_ms <= 0 {
199 return Err(Error::InvalidConfiguration {
200 field: "rebalance_timeout_ms",
201 reason: "must be greater than zero",
202 });
203 }
204 Ok(())
205 }
206
207 pub fn build_config(self) -> Result<Self> {
210 self.validate()?;
211 Ok(self)
212 }
213}
214
215pub struct StreamsGroupSession {
222 config: StreamsGroupConfig,
223 coordinator: Option<Client>,
224 member_id: String,
225 member_epoch: i32,
226 endpoint_information_epoch: i32,
227 heartbeat_interval: Duration,
228 pending_active_tasks: Option<Vec<StreamsGroupHeartbeatTask>>,
229 pending_standby_tasks: Option<Vec<StreamsGroupHeartbeatTask>>,
230 pending_warmup_tasks: Option<Vec<StreamsGroupHeartbeatTask>>,
231 pending_task_offsets: Option<Vec<StreamsGroupHeartbeatTaskOffset>>,
232 pending_task_end_offsets: Option<Vec<StreamsGroupHeartbeatTaskOffset>>,
233 assignment: StreamsGroupSessionAssignment,
234 closed: bool,
235}
236
237enum StreamsGroupCommand {
238 SetTaskState {
239 active_tasks: Vec<StreamsGroupHeartbeatTask>,
240 standby_tasks: Vec<StreamsGroupHeartbeatTask>,
241 warmup_tasks: Vec<StreamsGroupHeartbeatTask>,
242 task_offsets: Option<Vec<StreamsGroupHeartbeatTaskOffset>>,
243 task_end_offsets: Option<Vec<StreamsGroupHeartbeatTaskOffset>>,
244 acknowledged: oneshot::Sender<()>,
245 },
246 Heartbeat {
247 response: oneshot::Sender<Result<StreamsGroupHeartbeatResponseV0>>,
248 },
249 Close,
250}
251
252#[must_use = "a StreamsGroupSessionHandle must be closed or kept alive"]
262pub struct StreamsGroupSessionHandle {
263 commands: mpsc::Sender<StreamsGroupCommand>,
264 assignment: watch::Receiver<StreamsGroupSessionAssignment>,
265 task: Option<JoinHandle<Result<()>>>,
266}
267
268struct StreamsHeartbeatPayload {
269 member_epoch: i32,
270 topology: Option<StreamsGroupHeartbeatTopology>,
271 instance_id: Option<String>,
272 rack_id: Option<String>,
273 process_id: Option<String>,
274 user_endpoint: Option<StreamsGroupHeartbeatEndpoint>,
275 client_tags: Option<Vec<StreamsGroupHeartbeatKeyValue>>,
276 active_tasks: Option<Vec<StreamsGroupHeartbeatTask>>,
277 standby_tasks: Option<Vec<StreamsGroupHeartbeatTask>>,
278 warmup_tasks: Option<Vec<StreamsGroupHeartbeatTask>>,
279 task_offsets: Option<Vec<StreamsGroupHeartbeatTaskOffset>>,
280 task_end_offsets: Option<Vec<StreamsGroupHeartbeatTaskOffset>>,
281 shutdown_application: bool,
282}
283
284#[derive(Debug, Clone, Default, PartialEq, Eq)]
292pub struct StreamsGroupSessionAssignment {
293 pub status: Option<Vec<StreamsGroupHeartbeatStatus>>,
295 pub active_tasks: Option<Vec<StreamsGroupHeartbeatTask>>,
297 pub standby_tasks: Option<Vec<StreamsGroupHeartbeatTask>>,
299 pub warmup_tasks: Option<Vec<StreamsGroupHeartbeatTask>>,
301 pub acceptable_recovery_lag: i32,
303 pub task_offset_interval_ms: i32,
305 pub partitions_by_user_endpoint: Option<Vec<StreamsGroupHeartbeatEndpointPartitions>>,
307}
308
309impl StreamsGroupSessionAssignment {
310 fn from_response(response: &StreamsGroupHeartbeatResponseV0) -> Self {
311 Self {
312 status: response.status.clone(),
313 active_tasks: response.active_tasks.clone(),
314 standby_tasks: response.standby_tasks.clone(),
315 warmup_tasks: response.warmup_tasks.clone(),
316 acceptable_recovery_lag: response.acceptable_recovery_lag,
317 task_offset_interval_ms: response.task_offset_interval_ms,
318 partitions_by_user_endpoint: response.partitions_by_user_endpoint.clone(),
319 }
320 }
321}
322
323#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
325pub enum StreamsTaskRole {
326 Active,
328 Standby,
330 Warmup,
332}
333
334impl StreamsTaskRole {
335 pub const fn as_str(self) -> &'static str {
337 match self {
338 Self::Active => "active",
339 Self::Standby => "standby",
340 Self::Warmup => "warmup",
341 }
342 }
343}
344
345#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
351pub struct StreamsTaskId {
352 subtopology_id: String,
353 partitions: Vec<i32>,
354}
355
356impl StreamsTaskId {
357 pub fn new(subtopology_id: impl Into<String>, mut partitions: Vec<i32>) -> Result<Self> {
359 let subtopology_id = subtopology_id.into();
360 if subtopology_id.trim().is_empty() {
361 return Err(Error::StreamsTaskAssignmentInvalid {
362 subtopology_id,
363 reason: "subtopology ID must not be empty",
364 });
365 }
366 if partitions.is_empty() {
367 return Err(Error::StreamsTaskAssignmentInvalid {
368 subtopology_id,
369 reason: "task must contain at least one partition",
370 });
371 }
372 if partitions.iter().any(|partition| *partition < 0) {
373 return Err(Error::StreamsTaskAssignmentInvalid {
374 subtopology_id,
375 reason: "task partitions must not be negative",
376 });
377 }
378 partitions.sort_unstable();
379 if partitions.windows(2).any(|window| window[0] == window[1]) {
380 return Err(Error::StreamsTaskAssignmentInvalid {
381 subtopology_id,
382 reason: "task partitions must not contain duplicates",
383 });
384 }
385 Ok(Self {
386 subtopology_id,
387 partitions,
388 })
389 }
390
391 fn from_heartbeat_task(task: &StreamsGroupHeartbeatTask) -> Result<Self> {
392 Self::new(task.subtopology_id.clone(), task.partitions.clone())
393 }
394
395 pub fn subtopology_id(&self) -> &str {
397 &self.subtopology_id
398 }
399
400 pub fn partitions(&self) -> &[i32] {
402 &self.partitions
403 }
404}
405
406#[derive(Debug, Clone, PartialEq, Eq)]
408pub struct StreamsTaskAssignment {
409 pub task: StreamsTaskId,
411 pub role: StreamsTaskRole,
413}
414
415#[derive(Debug, Clone, PartialEq, Eq)]
417pub enum StreamsTaskTransition {
418 Added {
420 task: StreamsTaskId,
422 role: StreamsTaskRole,
424 },
425 Removed {
427 task: StreamsTaskId,
429 role: StreamsTaskRole,
431 },
432 RoleChanged {
434 task: StreamsTaskId,
436 previous_role: StreamsTaskRole,
438 role: StreamsTaskRole,
440 },
441}
442
443#[derive(Debug, Default)]
450pub struct StreamsTaskRuntime {
451 tasks: BTreeMap<StreamsTaskId, StreamsTaskRole>,
452}
453
454impl StreamsTaskRuntime {
455 pub fn new() -> Self {
457 Self::default()
458 }
459
460 pub fn assignment(&self) -> Vec<StreamsTaskAssignment> {
462 self.tasks
463 .iter()
464 .map(|(task, role)| StreamsTaskAssignment {
465 task: task.clone(),
466 role: *role,
467 })
468 .collect()
469 }
470
471 pub fn role(&self, task: &StreamsTaskId) -> Option<StreamsTaskRole> {
473 self.tasks.get(task).copied()
474 }
475
476 pub fn reconcile_assignment(
484 &mut self,
485 assignment: &StreamsGroupSessionAssignment,
486 ) -> Result<Vec<StreamsTaskTransition>> {
487 let mut desired = self.tasks.clone();
488 let updates = [
489 (StreamsTaskRole::Active, assignment.active_tasks.as_ref()),
490 (StreamsTaskRole::Standby, assignment.standby_tasks.as_ref()),
491 (StreamsTaskRole::Warmup, assignment.warmup_tasks.as_ref()),
492 ];
493
494 for (role, tasks) in updates {
495 if tasks.is_some() {
496 desired.retain(|_, current_role| *current_role != role);
497 }
498 }
499
500 let mut occupied = BTreeSet::new();
501 for task in desired.keys() {
502 for partition in task.partitions() {
503 if !occupied.insert((task.subtopology_id().to_owned(), *partition)) {
504 return Err(Error::StreamsTaskAssignmentConflict {
505 subtopology_id: task.subtopology_id().to_owned(),
506 partition: *partition,
507 });
508 }
509 }
510 }
511
512 for (role, tasks) in updates {
513 let Some(tasks) = tasks else {
514 continue;
515 };
516 for task in tasks {
517 let task = StreamsTaskId::from_heartbeat_task(task)?;
518 for partition in task.partitions() {
519 if !occupied.insert((task.subtopology_id().to_owned(), *partition)) {
520 return Err(Error::StreamsTaskAssignmentConflict {
521 subtopology_id: task.subtopology_id().to_owned(),
522 partition: *partition,
523 });
524 }
525 }
526 if desired.insert(task.clone(), role).is_some() {
527 return Err(Error::StreamsTaskAssignmentConflict {
528 subtopology_id: task.subtopology_id().to_owned(),
529 partition: task.partitions()[0],
530 });
531 }
532 }
533 }
534
535 let mut transitions = Vec::new();
536 for (task, previous_role) in &self.tasks {
537 match desired.get(task) {
538 None => transitions.push(StreamsTaskTransition::Removed {
539 task: task.clone(),
540 role: *previous_role,
541 }),
542 Some(role) if role != previous_role => {
543 transitions.push(StreamsTaskTransition::RoleChanged {
544 task: task.clone(),
545 previous_role: *previous_role,
546 role: *role,
547 });
548 }
549 Some(_) => {}
550 }
551 }
552 for (task, role) in &desired {
553 if !self.tasks.contains_key(task) {
554 transitions.push(StreamsTaskTransition::Added {
555 task: task.clone(),
556 role: *role,
557 });
558 }
559 }
560 self.tasks = desired;
561 Ok(transitions)
562 }
563}
564
565impl StreamsGroupSessionHandle {
566 pub fn reconcile_task_runtime(
568 &self,
569 runtime: &mut StreamsTaskRuntime,
570 ) -> Result<Vec<StreamsTaskTransition>> {
571 runtime.reconcile_assignment(&self.assignment())
572 }
573}
574
575impl StreamsGroupSession {
576 pub async fn join(config: StreamsGroupConfig) -> Result<Self> {
578 config.validate()?;
579 let mut session = Self {
580 config,
581 coordinator: None,
582 member_id: new_streams_member_id(),
583 member_epoch: 0,
584 endpoint_information_epoch: 0,
585 heartbeat_interval: Duration::from_secs(1),
586 pending_active_tasks: None,
587 pending_standby_tasks: None,
588 pending_warmup_tasks: None,
589 pending_task_offsets: None,
590 pending_task_end_offsets: None,
591 assignment: StreamsGroupSessionAssignment::default(),
592 closed: false,
593 };
594 session.join_with_retry().await?;
595 Ok(session)
596 }
597
598 pub fn group_id(&self) -> &str {
600 self.config.group_id()
601 }
602
603 pub fn member_id(&self) -> &str {
605 &self.member_id
606 }
607
608 pub fn member_epoch(&self) -> i32 {
610 self.member_epoch
611 }
612
613 pub fn heartbeat_interval(&self) -> Duration {
615 self.heartbeat_interval
616 }
617
618 pub fn assignment(&self) -> &StreamsGroupSessionAssignment {
624 &self.assignment
625 }
626
627 pub fn set_task_state(
634 &mut self,
635 active_tasks: Vec<StreamsGroupHeartbeatTask>,
636 standby_tasks: Vec<StreamsGroupHeartbeatTask>,
637 warmup_tasks: Vec<StreamsGroupHeartbeatTask>,
638 task_offsets: Vec<StreamsGroupHeartbeatTaskOffset>,
639 task_end_offsets: Vec<StreamsGroupHeartbeatTaskOffset>,
640 ) {
641 self.set_task_state_with_optional_offsets(
642 active_tasks,
643 standby_tasks,
644 warmup_tasks,
645 Some(task_offsets),
646 Some(task_end_offsets),
647 );
648 }
649
650 pub fn set_task_state_with_optional_offsets(
656 &mut self,
657 active_tasks: Vec<StreamsGroupHeartbeatTask>,
658 standby_tasks: Vec<StreamsGroupHeartbeatTask>,
659 warmup_tasks: Vec<StreamsGroupHeartbeatTask>,
660 task_offsets: Option<Vec<StreamsGroupHeartbeatTaskOffset>>,
661 task_end_offsets: Option<Vec<StreamsGroupHeartbeatTaskOffset>>,
662 ) {
663 self.pending_active_tasks = Some(active_tasks);
664 self.pending_standby_tasks = Some(standby_tasks);
665 self.pending_warmup_tasks = Some(warmup_tasks);
666 self.pending_task_offsets = task_offsets;
667 self.pending_task_end_offsets = task_end_offsets;
668 }
669
670 pub fn is_closed(&self) -> bool {
672 self.closed
673 }
674
675 pub async fn heartbeat(&mut self) -> Result<StreamsGroupHeartbeatResponseV0> {
681 self.ensure_open()?;
682 let mut retry = 0;
683 loop {
684 match self.send_heartbeat(false).await {
685 Ok(response) => return Ok(response),
686 Err(error)
687 if retry < self.config.max_retries && is_retryable_streams_error(&error) =>
688 {
689 retry += 1;
690 self.config.client.record_retry();
691 let rejoin = should_rejoin_streams_group(&error);
692 self.recover_streams_session(rejoin, &mut retry).await?;
693 sleep(streams_retry_backoff(retry)).await;
694 }
695 Err(error) => return Err(error),
696 }
697 }
698 }
699
700 pub async fn close(&mut self) -> Result<()> {
702 if self.closed {
703 return Ok(());
704 }
705 let mut retry = 0;
706 loop {
707 self.ensure_open()?;
708 match self.send_heartbeat(true).await {
709 Ok(_) => {
710 self.closed = true;
711 self.coordinator = None;
712 return Ok(());
713 }
714 Err(error)
715 if retry < self.config.max_retries && is_retryable_streams_error(&error) =>
716 {
717 retry += 1;
718 self.config.client.record_retry();
719 self.recover_streams_session(false, &mut retry).await?;
720 sleep(streams_retry_backoff(retry)).await;
721 }
722 Err(error) => return Err(error),
723 }
724 }
725 }
726
727 pub fn spawn_heartbeat_task(self) -> StreamsGroupSessionHandle {
734 let (commands, command_receiver) = mpsc::channel(STREAMS_GROUP_COMMAND_CAPACITY);
735 let (assignment_sender, assignment_receiver) = watch::channel(self.assignment.clone());
736 let task = tokio::spawn(run_streams_heartbeat_task(
737 self,
738 command_receiver,
739 assignment_sender,
740 ));
741 StreamsGroupSessionHandle {
742 commands,
743 assignment: assignment_receiver,
744 task: Some(task),
745 }
746 }
747
748 async fn join_with_retry(&mut self) -> Result<()> {
749 let mut retry = 0;
750 self.recover_streams_session(true, &mut retry).await
751 }
752
753 async fn recover_streams_session(&mut self, rejoin: bool, retry: &mut u32) -> Result<()> {
754 loop {
755 let result = async {
756 self.reconnect().await?;
757 if rejoin {
758 if self.member_epoch != 0 {
759 self.member_id = new_streams_member_id();
760 }
761 self.member_epoch = 0;
762 self.send_initial_heartbeat().await?;
763 }
764 Ok::<(), Error>(())
765 }
766 .await;
767
768 match result {
769 Ok(()) => return Ok(()),
770 Err(error)
771 if *retry < self.config.max_retries && is_retryable_streams_error(&error) =>
772 {
773 *retry += 1;
774 self.config.client.record_retry();
775 sleep(streams_retry_backoff(*retry)).await;
776 }
777 Err(error) => return Err(error),
778 }
779 }
780 }
781
782 async fn reconnect(&mut self) -> Result<()> {
783 self.coordinator = None;
784 let mut bootstrap = self.config.client.clone().connect().await?;
785 let coordinator = bootstrap
786 .find_group_coordinator(self.config.group_id.clone())
787 .await?;
788 if coordinator.error_code != 0 {
789 return Err(self.config.client.broker_error(
790 coordinator.error_code,
791 format!("find Streams group coordinator {}", self.config.group_id),
792 ));
793 }
794 let address = coordinator_addr(&coordinator);
795 let mut client = self.config.client.connect_broker(address).await?;
796 let versions = client
797 .api_versions_v3_cached("kafrust", env!("CARGO_PKG_VERSION"))
798 .await?;
799 ensure_streams_heartbeat_supported(&versions)?;
800 self.coordinator = Some(client);
801 Ok(())
802 }
803
804 async fn send_initial_heartbeat(&mut self) -> Result<StreamsGroupHeartbeatResponseV0> {
805 let response = self
806 .send_heartbeat_request(StreamsHeartbeatPayload {
807 member_epoch: 0,
808 topology: Some(self.config.topology.clone()),
809 instance_id: self.config.instance_id.clone(),
810 rack_id: self.config.rack_id.clone(),
811 process_id: self.config.process_id.clone(),
812 user_endpoint: self.config.user_endpoint.clone(),
813 client_tags: self.config.client_tags.clone(),
814 active_tasks: Some(Vec::new()),
815 standby_tasks: Some(Vec::new()),
816 warmup_tasks: Some(Vec::new()),
817 task_offsets: None,
818 task_end_offsets: None,
819 shutdown_application: false,
820 })
821 .await?;
822 self.apply_response(&response)?;
823 debug!(
824 group_id = self.config.group_id.as_str(),
825 member_id = self.member_id.as_str(),
826 member_epoch = self.member_epoch,
827 "joined Kafka Streams group"
828 );
829 Ok(response)
830 }
831
832 async fn send_heartbeat(&mut self, leave: bool) -> Result<StreamsGroupHeartbeatResponseV0> {
833 let member_epoch = if leave { -1 } else { self.member_epoch };
834 let active_tasks = if leave {
835 None
836 } else {
837 self.pending_active_tasks.clone()
838 };
839 let standby_tasks = if leave {
840 None
841 } else {
842 self.pending_standby_tasks.clone()
843 };
844 let warmup_tasks = if leave {
845 None
846 } else {
847 self.pending_warmup_tasks.clone()
848 };
849 let task_offsets = if leave {
850 None
851 } else {
852 self.pending_task_offsets.clone()
853 };
854 let task_end_offsets = if leave {
855 None
856 } else {
857 self.pending_task_end_offsets.clone()
858 };
859 let response = self
860 .send_heartbeat_request(StreamsHeartbeatPayload {
861 member_epoch,
862 topology: None,
863 instance_id: None,
864 rack_id: None,
865 process_id: None,
866 user_endpoint: None,
867 client_tags: None,
868 active_tasks,
869 standby_tasks,
870 warmup_tasks,
871 task_offsets,
872 task_end_offsets,
873 shutdown_application: leave,
874 })
875 .await?;
876 self.apply_response(&response)?;
877 if !leave {
878 self.pending_active_tasks = None;
879 self.pending_standby_tasks = None;
880 self.pending_warmup_tasks = None;
881 self.pending_task_offsets = None;
882 self.pending_task_end_offsets = None;
883 }
884 Ok(response)
885 }
886
887 async fn send_heartbeat_request(
888 &mut self,
889 payload: StreamsHeartbeatPayload,
890 ) -> Result<StreamsGroupHeartbeatResponseV0> {
891 let coordinator = self.coordinator.as_mut().ok_or(Error::Unsupported(
892 "Streams group coordinator is not connected",
893 ))?;
894 coordinator
895 .streams_group_heartbeat_v0(
896 self.config.group_id.clone(),
897 self.member_id.clone(),
898 payload.member_epoch,
899 self.endpoint_information_epoch,
900 payload.instance_id,
901 payload.rack_id,
902 if payload.member_epoch == -1 {
903 -1
904 } else {
905 self.config.rebalance_timeout_ms
906 },
907 payload.topology,
908 payload.active_tasks,
909 payload.standby_tasks,
910 payload.warmup_tasks,
911 payload.process_id,
912 payload.user_endpoint,
913 payload.client_tags,
914 payload.task_offsets,
915 payload.task_end_offsets,
916 payload.shutdown_application,
917 )
918 .await
919 }
920
921 fn apply_response(&mut self, response: &StreamsGroupHeartbeatResponseV0) -> Result<()> {
922 if response.error_code != 0 {
923 return Err(self.config.client.broker_error(
924 response.error_code,
925 format!(
926 "Streams group heartbeat {}: {}",
927 self.config.group_id,
928 response
929 .error_message
930 .as_deref()
931 .unwrap_or("broker returned a Streams group error")
932 ),
933 ));
934 }
935 self.member_id = response.member_id.clone();
936 self.member_epoch = response.member_epoch;
937 self.endpoint_information_epoch = response.endpoint_information_epoch;
938 self.assignment = StreamsGroupSessionAssignment::from_response(response);
939 if response.heartbeat_interval_ms > 0 {
940 self.heartbeat_interval = Duration::from_millis(
941 u64::try_from(response.heartbeat_interval_ms).unwrap_or(u64::MAX),
942 );
943 }
944 Ok(())
945 }
946
947 fn ensure_open(&self) -> Result<()> {
948 if self.closed {
949 return Err(Error::Unsupported("Streams group session is closed"));
950 }
951 Ok(())
952 }
953}
954
955impl StreamsGroupSessionHandle {
956 pub fn assignment(&self) -> StreamsGroupSessionAssignment {
958 self.assignment.borrow().clone()
959 }
960
961 pub fn subscribe_assignment(&self) -> watch::Receiver<StreamsGroupSessionAssignment> {
965 self.assignment.clone()
966 }
967
968 pub async fn set_task_state(
970 &self,
971 active_tasks: Vec<StreamsGroupHeartbeatTask>,
972 standby_tasks: Vec<StreamsGroupHeartbeatTask>,
973 warmup_tasks: Vec<StreamsGroupHeartbeatTask>,
974 task_offsets: Option<Vec<StreamsGroupHeartbeatTaskOffset>>,
975 task_end_offsets: Option<Vec<StreamsGroupHeartbeatTaskOffset>>,
976 ) -> Result<()> {
977 let (acknowledged, completion) = oneshot::channel();
978 self.commands
979 .send(StreamsGroupCommand::SetTaskState {
980 active_tasks,
981 standby_tasks,
982 warmup_tasks,
983 task_offsets,
984 task_end_offsets,
985 acknowledged,
986 })
987 .await
988 .map_err(|_| Error::StreamsGroupBackgroundTaskClosed)?;
989 completion
990 .await
991 .map_err(|_| Error::StreamsGroupBackgroundTaskClosed)
992 }
993
994 pub async fn heartbeat_now(&self) -> Result<StreamsGroupHeartbeatResponseV0> {
997 let (response, completion) = oneshot::channel();
998 self.commands
999 .send(StreamsGroupCommand::Heartbeat { response })
1000 .await
1001 .map_err(|_| Error::StreamsGroupBackgroundTaskClosed)?;
1002 completion
1003 .await
1004 .map_err(|_| Error::StreamsGroupBackgroundTaskClosed)?
1005 }
1006
1007 pub async fn close(mut self) -> Result<()> {
1011 let Some(task) = self.task.take() else {
1012 return Ok(());
1013 };
1014 if self
1015 .commands
1016 .send(StreamsGroupCommand::Close)
1017 .await
1018 .is_err()
1019 {
1020 return task.await.map_err(Error::from)?;
1021 }
1022 task.await.map_err(Error::from)?
1023 }
1024}
1025
1026impl Drop for StreamsGroupSessionHandle {
1027 fn drop(&mut self) {
1028 if let Some(task) = self.task.as_ref() {
1029 task.abort();
1030 }
1031 }
1032}
1033
1034async fn run_streams_heartbeat_task(
1035 mut session: StreamsGroupSession,
1036 mut commands: mpsc::Receiver<StreamsGroupCommand>,
1037 assignment_sender: watch::Sender<StreamsGroupSessionAssignment>,
1038) -> Result<()> {
1039 loop {
1040 let heartbeat = sleep(session.heartbeat_interval());
1041 tokio::pin!(heartbeat);
1042 tokio::select! {
1043 command = commands.recv() => match command {
1044 Some(StreamsGroupCommand::SetTaskState {
1045 active_tasks,
1046 standby_tasks,
1047 warmup_tasks,
1048 task_offsets,
1049 task_end_offsets,
1050 acknowledged,
1051 }) => {
1052 session.set_task_state_with_optional_offsets(
1053 active_tasks,
1054 standby_tasks,
1055 warmup_tasks,
1056 task_offsets,
1057 task_end_offsets,
1058 );
1059 let _ = acknowledged.send(());
1060 }
1061 Some(StreamsGroupCommand::Heartbeat { response }) => {
1062 let result = session.heartbeat().await;
1063 if result.is_ok() {
1064 let _ = assignment_sender.send(session.assignment.clone());
1065 }
1066 let _ = response.send(result);
1067 }
1068 Some(StreamsGroupCommand::Close) => return session.close().await,
1069 None => return Ok(()),
1070 },
1071 _ = &mut heartbeat => {
1072 session.heartbeat().await?;
1073 let _ = assignment_sender.send(session.assignment.clone());
1074 }
1075 }
1076 }
1077}
1078
1079fn ensure_streams_heartbeat_supported(versions: &ApiVersionsResponseV3) -> Result<()> {
1080 if versions
1081 .highest_supported_version(STREAMS_GROUP_HEARTBEAT_API_KEY, 0)
1082 .is_none()
1083 {
1084 return Err(Error::Unsupported(
1085 "broker does not advertise StreamsGroupHeartbeat v0",
1086 ));
1087 }
1088 Ok(())
1089}
1090
1091fn coordinator_addr(coordinator: &FindCoordinatorResponseV1) -> String {
1092 format!("{}:{}", coordinator.host, coordinator.port)
1093}
1094
1095fn new_streams_member_id() -> String {
1096 use rand::RngCore;
1097
1098 let mut bytes = [0_u8; 16];
1099 rand::thread_rng().fill_bytes(&mut bytes);
1100 bytes[6] = (bytes[6] & 0x0f) | 0x40;
1101 bytes[8] = (bytes[8] & 0x3f) | 0x80;
1102 let hex = bytes
1103 .iter()
1104 .fold(String::with_capacity(32), |mut hex, byte| {
1105 use std::fmt::Write;
1106
1107 let _ = write!(hex, "{byte:02x}");
1108 hex
1109 });
1110 format!(
1111 "{}-{}-{}-{}-{}",
1112 &hex[..8],
1113 &hex[8..12],
1114 &hex[12..16],
1115 &hex[16..20],
1116 &hex[20..]
1117 )
1118}
1119
1120fn is_retryable_streams_error(error: &Error) -> bool {
1121 if matches!(error, Error::Io(_)) {
1122 return true;
1123 }
1124 matches!(
1125 error.broker_error_kind(),
1126 Some(
1127 BrokerErrorKind::CoordinatorLoadInProgress
1128 | BrokerErrorKind::CoordinatorNotAvailable
1129 | BrokerErrorKind::NotCoordinator
1130 | BrokerErrorKind::RebalanceInProgress
1131 | BrokerErrorKind::UnknownMemberId
1132 | BrokerErrorKind::FencedMemberEpoch
1133 | BrokerErrorKind::StaleMemberEpoch
1134 )
1135 )
1136}
1137
1138fn should_rejoin_streams_group(error: &Error) -> bool {
1139 matches!(
1140 error.broker_error_kind(),
1141 Some(
1142 BrokerErrorKind::UnknownMemberId
1143 | BrokerErrorKind::FencedMemberEpoch
1144 | BrokerErrorKind::StaleMemberEpoch
1145 )
1146 )
1147}
1148
1149fn streams_retry_backoff(attempt: u32) -> Duration {
1150 let factor = u32::pow(2, attempt.saturating_sub(1).min(5));
1151 STREAMS_GROUP_RETRY_BACKOFF
1152 .checked_mul(factor)
1153 .unwrap_or(STREAMS_GROUP_MAX_RETRY_BACKOFF)
1154 .min(STREAMS_GROUP_MAX_RETRY_BACKOFF)
1155}
1156
1157#[cfg(test)]
1158#[allow(clippy::unwrap_used)]
1159mod tests {
1160 use super::{
1161 streams_retry_backoff, StreamsGroupConfig, StreamsGroupHeartbeatEndpoint,
1162 StreamsGroupHeartbeatKeyValue, StreamsGroupHeartbeatTask, StreamsGroupHeartbeatTaskOffset,
1163 StreamsGroupHeartbeatTopology, StreamsGroupSession, StreamsGroupSessionAssignment,
1164 StreamsTaskRole, StreamsTaskRuntime, StreamsTaskTransition,
1165 STREAMS_GROUP_MAX_RETRY_BACKOFF,
1166 };
1167 use crate::client::Client;
1168 use crate::error::Error;
1169 use kafrust_protocol::api::streams_group_heartbeat::StreamsGroupHeartbeatSubtopology;
1170 use kafrust_protocol::codec::{Decoder, Encoder};
1171 use std::time::Duration;
1172 use tokio::io::{AsyncReadExt, AsyncWriteExt};
1173
1174 fn topology() -> StreamsGroupHeartbeatTopology {
1175 StreamsGroupHeartbeatTopology {
1176 epoch: 1,
1177 subtopologies: vec![StreamsGroupHeartbeatSubtopology {
1178 subtopology_id: "subtopology-0".to_owned(),
1179 source_topics: vec!["orders".to_owned()],
1180 source_topic_regex: Vec::new(),
1181 state_changelog_topics: Vec::new(),
1182 repartition_sink_topics: Vec::new(),
1183 repartition_source_topics: Vec::new(),
1184 copartition_groups: Vec::new(),
1185 }],
1186 }
1187 }
1188
1189 trait DecoderLengthExt {
1190 fn read_compact_array_length(&mut self) -> Option<usize>;
1191 }
1192
1193 impl<'a> DecoderLengthExt for Decoder<'a> {
1194 fn read_compact_array_length(&mut self) -> Option<usize> {
1195 match self.read_unsigned_varint().unwrap() {
1196 0 => None,
1197 length => Some(usize::try_from(length - 1).unwrap()),
1198 }
1199 }
1200 }
1201
1202 #[test]
1203 fn validates_required_streams_topology() {
1204 let config = StreamsGroupConfig::new(["localhost:9092"], "orders", topology());
1205 assert_eq!(config.group_id(), "orders");
1206 assert!(config.validate().is_ok());
1207 }
1208
1209 #[test]
1210 fn rejects_empty_streams_topology() {
1211 let config = StreamsGroupConfig::new(
1212 ["localhost:9092"],
1213 "orders",
1214 StreamsGroupHeartbeatTopology {
1215 epoch: 1,
1216 subtopologies: Vec::new(),
1217 },
1218 );
1219 assert!(config.validate().is_err());
1220 }
1221
1222 #[test]
1223 fn caps_streams_retry_backoff() {
1224 assert_eq!(streams_retry_backoff(100), STREAMS_GROUP_MAX_RETRY_BACKOFF);
1225 assert_eq!(streams_retry_backoff(1), Duration::from_millis(50));
1226 }
1227
1228 #[test]
1229 fn generates_uuid_shaped_streams_member_ids() {
1230 let member_id = super::new_streams_member_id();
1231 assert_eq!(member_id.len(), 36);
1232 assert_eq!(&member_id[8..9], "-");
1233 assert_eq!(&member_id[13..14], "-");
1234 assert_eq!(&member_id[18..19], "-");
1235 assert_eq!(&member_id[23..24], "-");
1236 assert_eq!(&member_id[14..15], "4");
1237 assert!(matches!(&member_id[19..20], "8" | "9" | "a" | "b"));
1238 }
1239
1240 #[tokio::test]
1241 async fn preserves_streams_group_lifecycle_on_the_wire() {
1242 let (client_stream, mut broker_stream) = tokio::io::duplex(16 * 1024);
1243 let broker = tokio::spawn(async move {
1244 let request = read_frame(&mut broker_stream).await;
1245 let (correlation_id, mut decoder) = request_header(&request);
1246 assert_eq!(correlation_id, 1);
1247 assert_eq!(decoder.read_compact_string().unwrap(), "orders");
1248 assert_eq!(decoder.read_compact_string().unwrap(), "client-member");
1249 assert_eq!(decoder.read_i32().unwrap(), 0);
1250 assert_eq!(decoder.read_i32().unwrap(), 0);
1251 assert_eq!(
1252 decoder.read_compact_nullable_string().unwrap(),
1253 Some("instance-a".to_owned())
1254 );
1255 assert_eq!(
1256 decoder.read_compact_nullable_string().unwrap(),
1257 Some("rack-a".to_owned())
1258 );
1259 assert_eq!(decoder.read_i32().unwrap(), 30_000);
1260 assert_eq!(decoder.read_i8().unwrap(), 1);
1261 assert_eq!(decoder.read_i32().unwrap(), 1);
1262 assert_eq!(decoder.read_compact_array_length(), Some(1));
1263 assert_eq!(decoder.read_compact_string().unwrap(), "subtopology-0");
1264 assert_eq!(decoder.read_compact_array_length(), Some(1));
1265 assert_eq!(decoder.read_compact_string().unwrap(), "orders");
1266 assert_eq!(decoder.read_compact_array_length(), Some(0));
1267 assert_eq!(decoder.read_compact_array_length(), Some(0));
1268 assert_eq!(decoder.read_compact_array_length(), Some(0));
1269 assert_eq!(decoder.read_compact_array_length(), Some(0));
1270 assert_eq!(decoder.read_compact_array_length(), Some(0));
1271 decoder.read_tagged_fields().unwrap();
1272 decoder.read_tagged_fields().unwrap();
1273 assert_eq!(decoder.read_compact_array_length(), Some(0));
1274 assert_eq!(decoder.read_compact_array_length(), Some(0));
1275 assert_eq!(decoder.read_compact_array_length(), Some(0));
1276 assert_eq!(
1277 decoder.read_compact_nullable_string().unwrap(),
1278 Some("process-a".to_owned())
1279 );
1280 assert_eq!(decoder.read_i8().unwrap(), 1);
1281 assert_eq!(decoder.read_compact_string().unwrap(), "query-host");
1282 assert_eq!(decoder.read_i16().unwrap(), 7_777);
1283 decoder.read_tagged_fields().unwrap();
1284 assert_eq!(decoder.read_compact_array_length(), Some(1));
1285 assert_eq!(decoder.read_compact_string().unwrap(), "zone");
1286 assert_eq!(decoder.read_compact_string().unwrap(), "a");
1287 decoder.read_tagged_fields().unwrap();
1288 assert_eq!(decoder.read_compact_array_length(), None);
1289 assert_eq!(decoder.read_compact_array_length(), None);
1290 assert!(!decoder.read_bool().unwrap());
1291 decoder.read_tagged_fields().unwrap();
1292 write_streams_response(&mut broker_stream, correlation_id, "member-1", 2, 4).await;
1293
1294 let request = read_frame(&mut broker_stream).await;
1295 let (correlation_id, mut decoder) = request_header(&request);
1296 assert_eq!(correlation_id, 2);
1297 assert_eq!(decoder.read_compact_string().unwrap(), "orders");
1298 assert_eq!(decoder.read_compact_string().unwrap(), "member-1");
1299 assert_eq!(decoder.read_i32().unwrap(), 2);
1300 assert_eq!(decoder.read_i32().unwrap(), 4);
1301 assert_eq!(decoder.read_compact_nullable_string().unwrap(), None);
1302 assert_eq!(decoder.read_compact_nullable_string().unwrap(), None);
1303 assert_eq!(decoder.read_i32().unwrap(), 30_000);
1304 assert_eq!(decoder.read_i8().unwrap(), -1);
1305 assert_eq!(decoder.read_compact_array_length(), Some(1));
1306 assert_eq!(decoder.read_compact_string().unwrap(), "subtopology-0");
1307 assert_eq!(decoder.read_compact_array_length(), Some(2));
1308 assert_eq!(decoder.read_i32().unwrap(), 0);
1309 assert_eq!(decoder.read_i32().unwrap(), 1);
1310 decoder.read_tagged_fields().unwrap();
1311 assert_eq!(decoder.read_compact_array_length(), Some(0));
1312 assert_eq!(decoder.read_compact_array_length(), Some(0));
1313 assert_eq!(decoder.read_compact_nullable_string().unwrap(), None);
1314 assert_eq!(decoder.read_i8().unwrap(), -1);
1315 assert_eq!(decoder.read_compact_array_length(), None);
1316 assert_eq!(decoder.read_compact_array_length(), Some(1));
1317 assert_eq!(decoder.read_compact_string().unwrap(), "subtopology-0");
1318 assert_eq!(decoder.read_i32().unwrap(), 0);
1319 assert_eq!(decoder.read_i64().unwrap(), 10);
1320 decoder.read_tagged_fields().unwrap();
1321 assert_eq!(decoder.read_compact_array_length(), Some(1));
1322 assert_eq!(decoder.read_compact_string().unwrap(), "subtopology-0");
1323 assert_eq!(decoder.read_i32().unwrap(), 0);
1324 assert_eq!(decoder.read_i64().unwrap(), 20);
1325 decoder.read_tagged_fields().unwrap();
1326 assert!(!decoder.read_bool().unwrap());
1327 decoder.read_tagged_fields().unwrap();
1328 write_streams_response(&mut broker_stream, correlation_id, "member-1", 3, 5).await;
1329
1330 let request = read_frame(&mut broker_stream).await;
1331 let (correlation_id, mut decoder) = request_header(&request);
1332 assert_eq!(correlation_id, 3);
1333 assert_eq!(decoder.read_compact_string().unwrap(), "orders");
1334 assert_eq!(decoder.read_compact_string().unwrap(), "member-1");
1335 assert_eq!(decoder.read_i32().unwrap(), -1);
1336 assert_eq!(decoder.read_i32().unwrap(), 5);
1337 assert_eq!(decoder.read_compact_nullable_string().unwrap(), None);
1338 assert_eq!(decoder.read_compact_nullable_string().unwrap(), None);
1339 assert_eq!(decoder.read_i32().unwrap(), -1);
1340 assert_eq!(decoder.read_i8().unwrap(), -1);
1341 assert_eq!(decoder.read_compact_array_length(), None);
1342 assert_eq!(decoder.read_compact_array_length(), None);
1343 assert_eq!(decoder.read_compact_array_length(), None);
1344 assert_eq!(decoder.read_compact_nullable_string().unwrap(), None);
1345 assert_eq!(decoder.read_i8().unwrap(), -1);
1346 assert_eq!(decoder.read_compact_array_length(), None);
1347 assert_eq!(decoder.read_compact_array_length(), None);
1348 assert_eq!(decoder.read_compact_array_length(), None);
1349 assert!(decoder.read_bool().unwrap());
1350 decoder.read_tagged_fields().unwrap();
1351 write_streams_response(&mut broker_stream, correlation_id, "member-1", -1, 5).await;
1352 });
1353
1354 let config = StreamsGroupConfig::new(["localhost:9092"], "orders", topology())
1355 .client_id("streams-test")
1356 .instance_id("instance-a")
1357 .rack_id("rack-a")
1358 .process_id("process-a")
1359 .user_endpoint(StreamsGroupHeartbeatEndpoint {
1360 host: "query-host".to_owned(),
1361 port: 7_777,
1362 })
1363 .client_tags(vec![StreamsGroupHeartbeatKeyValue {
1364 key: "zone".to_owned(),
1365 value: "a".to_owned(),
1366 }])
1367 .max_retries(0);
1368 let client = Client::from_stream(
1369 Box::new(client_stream),
1370 Some("streams-test".to_owned()),
1371 Some(Duration::from_secs(1)),
1372 );
1373 let mut session = StreamsGroupSession {
1374 config,
1375 coordinator: Some(client),
1376 member_id: "client-member".to_owned(),
1377 member_epoch: 0,
1378 endpoint_information_epoch: 0,
1379 heartbeat_interval: Duration::from_secs(1),
1380 pending_active_tasks: None,
1381 pending_standby_tasks: None,
1382 pending_warmup_tasks: None,
1383 pending_task_offsets: None,
1384 pending_task_end_offsets: None,
1385 assignment: StreamsGroupSessionAssignment::default(),
1386 closed: false,
1387 };
1388
1389 session.send_initial_heartbeat().await.unwrap();
1390 assert_eq!(session.member_id(), "member-1");
1391 assert_eq!(session.member_epoch(), 2);
1392 assert_eq!(session.assignment().task_offset_interval_ms, 100);
1393 assert_eq!(session.assignment().acceptable_recovery_lag, 0);
1394 assert!(session.assignment().active_tasks.is_none());
1395 session.set_task_state(
1396 vec![StreamsGroupHeartbeatTask {
1397 subtopology_id: "subtopology-0".to_owned(),
1398 partitions: vec![0, 1],
1399 }],
1400 Vec::new(),
1401 Vec::new(),
1402 vec![StreamsGroupHeartbeatTaskOffset {
1403 subtopology_id: "subtopology-0".to_owned(),
1404 partition: 0,
1405 offset: 10,
1406 }],
1407 vec![StreamsGroupHeartbeatTaskOffset {
1408 subtopology_id: "subtopology-0".to_owned(),
1409 partition: 0,
1410 offset: 20,
1411 }],
1412 );
1413 session.heartbeat().await.unwrap();
1414 assert_eq!(session.member_epoch(), 3);
1415 session.close().await.unwrap();
1416 assert!(session.is_closed());
1417 broker.await.unwrap();
1418 }
1419
1420 #[tokio::test]
1421 async fn background_streams_session_owns_heartbeat_and_graceful_close() {
1422 let (client_stream, mut broker_stream) = tokio::io::duplex(16 * 1024);
1423 let broker = tokio::spawn(async move {
1424 let request = read_frame(&mut broker_stream).await;
1425 let (correlation_id, mut decoder) = request_header(&request);
1426 assert_eq!(decoder.read_compact_string().unwrap(), "orders");
1427 assert_eq!(decoder.read_compact_string().unwrap(), "member-1");
1428 assert_eq!(decoder.read_i32().unwrap(), 2);
1429 assert_eq!(decoder.read_i32().unwrap(), 4);
1430 assert_eq!(decoder.read_compact_nullable_string().unwrap(), None);
1431 assert_eq!(decoder.read_compact_nullable_string().unwrap(), None);
1432 assert_eq!(decoder.read_i32().unwrap(), 30_000);
1433 assert_eq!(decoder.read_i8().unwrap(), -1);
1434 assert_eq!(decoder.read_compact_array_length(), Some(0));
1435 assert_eq!(decoder.read_compact_array_length(), Some(0));
1436 assert_eq!(decoder.read_compact_array_length(), Some(0));
1437 assert_eq!(decoder.read_compact_nullable_string().unwrap(), None);
1438 assert_eq!(decoder.read_i8().unwrap(), -1);
1439 assert_eq!(decoder.read_compact_array_length(), None);
1440 assert_eq!(decoder.read_compact_array_length(), Some(0));
1441 assert_eq!(decoder.read_compact_array_length(), Some(0));
1442 assert!(!decoder.read_bool().unwrap());
1443 decoder.read_tagged_fields().unwrap();
1444 write_streams_response(&mut broker_stream, correlation_id, "member-1", 3, 5).await;
1445
1446 let request = read_frame(&mut broker_stream).await;
1447 let (correlation_id, mut decoder) = request_header(&request);
1448 assert_eq!(decoder.read_compact_string().unwrap(), "orders");
1449 assert_eq!(decoder.read_compact_string().unwrap(), "member-1");
1450 assert_eq!(decoder.read_i32().unwrap(), -1);
1451 assert_eq!(decoder.read_i32().unwrap(), 5);
1452 assert_eq!(decoder.read_compact_nullable_string().unwrap(), None);
1453 assert_eq!(decoder.read_compact_nullable_string().unwrap(), None);
1454 assert_eq!(decoder.read_i32().unwrap(), -1);
1455 assert_eq!(decoder.read_i8().unwrap(), -1);
1456 assert_eq!(decoder.read_compact_array_length(), None);
1457 assert_eq!(decoder.read_compact_array_length(), None);
1458 assert_eq!(decoder.read_compact_array_length(), None);
1459 assert_eq!(decoder.read_compact_nullable_string().unwrap(), None);
1460 assert_eq!(decoder.read_i8().unwrap(), -1);
1461 assert_eq!(decoder.read_compact_array_length(), None);
1462 assert_eq!(decoder.read_compact_array_length(), None);
1463 assert_eq!(decoder.read_compact_array_length(), None);
1464 assert!(decoder.read_bool().unwrap());
1465 decoder.read_tagged_fields().unwrap();
1466 write_streams_response(&mut broker_stream, correlation_id, "member-1", -1, 5).await;
1467 });
1468
1469 let config = StreamsGroupConfig::new(["localhost:9092"], "orders", topology())
1470 .client_id("streams-test")
1471 .max_retries(0);
1472 let client = Client::from_stream(
1473 Box::new(client_stream),
1474 Some("streams-test".to_owned()),
1475 Some(Duration::from_secs(1)),
1476 );
1477 let session = StreamsGroupSession {
1478 config,
1479 coordinator: Some(client),
1480 member_id: "member-1".to_owned(),
1481 member_epoch: 2,
1482 endpoint_information_epoch: 4,
1483 heartbeat_interval: Duration::from_millis(100),
1484 pending_active_tasks: None,
1485 pending_standby_tasks: None,
1486 pending_warmup_tasks: None,
1487 pending_task_offsets: None,
1488 pending_task_end_offsets: None,
1489 assignment: StreamsGroupSessionAssignment::default(),
1490 closed: false,
1491 };
1492
1493 let handle = session.spawn_heartbeat_task();
1494 handle
1495 .set_task_state(
1496 Vec::new(),
1497 Vec::new(),
1498 Vec::new(),
1499 Some(Vec::new()),
1500 Some(Vec::new()),
1501 )
1502 .await
1503 .unwrap();
1504 let mut assignments = handle.subscribe_assignment();
1505 tokio::time::timeout(Duration::from_secs(2), assignments.changed())
1506 .await
1507 .unwrap()
1508 .unwrap();
1509 assert_eq!(handle.assignment().task_offset_interval_ms, 100);
1510 handle.close().await.unwrap();
1511 broker.await.unwrap();
1512 }
1513
1514 fn assignment(
1515 active_tasks: Option<Vec<StreamsGroupHeartbeatTask>>,
1516 standby_tasks: Option<Vec<StreamsGroupHeartbeatTask>>,
1517 warmup_tasks: Option<Vec<StreamsGroupHeartbeatTask>>,
1518 ) -> StreamsGroupSessionAssignment {
1519 StreamsGroupSessionAssignment {
1520 active_tasks,
1521 standby_tasks,
1522 warmup_tasks,
1523 ..StreamsGroupSessionAssignment::default()
1524 }
1525 }
1526
1527 fn task(subtopology_id: &str, partitions: &[i32]) -> StreamsGroupHeartbeatTask {
1528 StreamsGroupHeartbeatTask {
1529 subtopology_id: subtopology_id.to_owned(),
1530 partitions: partitions.to_vec(),
1531 }
1532 }
1533
1534 #[test]
1535 fn task_runtime_reconciles_nullable_roles_and_canonical_task_ids() {
1536 let mut runtime = StreamsTaskRuntime::new();
1537 let transitions = runtime
1538 .reconcile_assignment(&assignment(
1539 Some(vec![task("subtopology-0", &[2, 1])]),
1540 Some(vec![task("subtopology-0", &[3])]),
1541 Some(Vec::new()),
1542 ))
1543 .unwrap();
1544 assert_eq!(transitions.len(), 2);
1545 assert!(matches!(
1546 &transitions[0],
1547 StreamsTaskTransition::Added {
1548 role: StreamsTaskRole::Active,
1549 ..
1550 }
1551 ));
1552 assert!(matches!(
1553 &transitions[1],
1554 StreamsTaskTransition::Added {
1555 role: StreamsTaskRole::Standby,
1556 ..
1557 }
1558 ));
1559 assert_eq!(runtime.assignment()[0].task.partitions(), &[1, 2]);
1560
1561 let unchanged = runtime
1562 .reconcile_assignment(&assignment(None, None, None))
1563 .unwrap();
1564 assert!(unchanged.is_empty());
1565
1566 let transitions = runtime
1567 .reconcile_assignment(&assignment(
1568 Some(Vec::new()),
1569 Some(vec![task("subtopology-0", &[2, 1])]),
1570 None,
1571 ))
1572 .unwrap();
1573 assert_eq!(
1574 transitions,
1575 vec![
1576 StreamsTaskTransition::RoleChanged {
1577 task: super::StreamsTaskId::new("subtopology-0", vec![1, 2]).unwrap(),
1578 previous_role: StreamsTaskRole::Active,
1579 role: StreamsTaskRole::Standby,
1580 },
1581 StreamsTaskTransition::Removed {
1582 task: super::StreamsTaskId::new("subtopology-0", vec![3]).unwrap(),
1583 role: StreamsTaskRole::Standby,
1584 },
1585 ]
1586 );
1587 }
1588
1589 #[test]
1590 fn task_runtime_rejects_conflicting_assignment_without_mutating_state() {
1591 let mut runtime = StreamsTaskRuntime::new();
1592 runtime
1593 .reconcile_assignment(&assignment(
1594 Some(vec![task("subtopology-0", &[0])]),
1595 Some(Vec::new()),
1596 Some(Vec::new()),
1597 ))
1598 .unwrap();
1599 let before = runtime.assignment();
1600
1601 let error = runtime
1602 .reconcile_assignment(&assignment(
1603 Some(vec![
1604 task("subtopology-0", &[1]),
1605 task("subtopology-0", &[1]),
1606 ]),
1607 Some(Vec::new()),
1608 Some(Vec::new()),
1609 ))
1610 .unwrap_err();
1611 assert!(matches!(
1612 error,
1613 Error::StreamsTaskAssignmentConflict {
1614 subtopology_id,
1615 partition: 1,
1616 } if subtopology_id == "subtopology-0"
1617 ));
1618 assert_eq!(runtime.assignment(), before);
1619 }
1620
1621 fn request_header(request: &[u8]) -> (i32, Decoder<'_>) {
1622 let mut decoder = Decoder::new(request);
1623 assert_eq!(decoder.read_i16().unwrap(), 88);
1624 assert_eq!(decoder.read_i16().unwrap(), 0);
1625 let correlation_id = decoder.read_i32().unwrap();
1626 assert_eq!(
1627 decoder.read_nullable_string().unwrap(),
1628 Some("streams-test".to_owned())
1629 );
1630 decoder.read_tagged_fields().unwrap();
1631 (correlation_id, decoder)
1632 }
1633
1634 async fn write_streams_response(
1635 stream: &mut tokio::io::DuplexStream,
1636 correlation_id: i32,
1637 member_id: &str,
1638 member_epoch: i32,
1639 endpoint_information_epoch: i32,
1640 ) {
1641 let mut response = Encoder::new();
1642 response.write_i32(correlation_id);
1643 response.write_empty_tagged_fields();
1644 response.write_i32(0);
1645 response.write_i16(0);
1646 response.write_compact_nullable_string(None).unwrap();
1647 response.write_compact_string(member_id).unwrap();
1648 response.write_i32(member_epoch);
1649 response.write_i32(1_000);
1650 response.write_i32(0);
1651 response.write_i32(100);
1652 response.write_unsigned_varint(0);
1653 response.write_unsigned_varint(0);
1654 response.write_unsigned_varint(0);
1655 response.write_unsigned_varint(0);
1656 response.write_i32(endpoint_information_epoch);
1657 response.write_unsigned_varint(0);
1658 response.write_empty_tagged_fields();
1659 write_frame(stream, &response.into_bytes()).await;
1660 }
1661
1662 async fn read_frame(stream: &mut tokio::io::DuplexStream) -> Vec<u8> {
1663 let mut size = [0; 4];
1664 stream.read_exact(&mut size).await.unwrap();
1665 let size = usize::try_from(i32::from_be_bytes(size)).unwrap();
1666 let mut frame = vec![0; size];
1667 stream.read_exact(&mut frame).await.unwrap();
1668 frame
1669 }
1670
1671 async fn write_frame(stream: &mut tokio::io::DuplexStream, frame: &[u8]) {
1672 stream
1673 .write_all(&(i32::try_from(frame.len()).unwrap()).to_be_bytes())
1674 .await
1675 .unwrap();
1676 stream.write_all(frame).await.unwrap();
1677 stream.flush().await.unwrap();
1678 }
1679}