1use std::collections::BTreeMap;
20use std::time::Duration;
21
22use barnabas_core::group::{Assignor, Subscription, TopicPartition};
23use barnabas_core::member::{codes, GroupMember, Step};
24use bytes::Bytes;
25use kafka_protocol::messages::{
26 consumer_protocol_assignment::{
27 ConsumerProtocolAssignment, TopicPartition as AssignmentTopicPartition,
28 },
29 consumer_protocol_subscription::{
30 ConsumerProtocolSubscription, TopicPartition as SubscriptionTopicPartition,
31 },
32 join_group_request::{JoinGroupRequest, JoinGroupRequestProtocol},
33 offset_commit_request::{OffsetCommitRequestPartition, OffsetCommitRequestTopic},
34 offset_fetch_request::OffsetFetchRequestTopic,
35 sync_group_request::{SyncGroupRequest, SyncGroupRequestAssignment},
36 ApiKey, FindCoordinatorRequest, FindCoordinatorResponse, GroupId, HeartbeatRequest,
37 HeartbeatResponse, JoinGroupResponse, LeaveGroupRequest, LeaveGroupResponse,
38 OffsetCommitRequest, OffsetCommitResponse, OffsetFetchRequest, OffsetFetchResponse,
39 SyncGroupResponse, TopicName,
40};
41use kafka_protocol::protocol::{Decodable, Encodable, StrBytes};
42
43use crate::cluster::Cluster;
44use crate::{Error, Result, Transport};
45
46const CONSUMER_PROTOCOL: &str = "consumer";
50
51const COORDINATOR_RETRIES: usize = 40;
53const COORDINATOR_BACKOFF: Duration = Duration::from_millis(250);
54
55const JOIN_SLACK: Duration = Duration::from_secs(5);
58
59const COORDINATOR_TIMEOUT: Duration = Duration::from_secs(30);
61
62const PROTOCOL_VERSION: i16 = 2;
73
74fn read_version(cursor: &mut Bytes) -> Result<i16> {
76 use bytes::Buf;
77 if cursor.remaining() < 2 {
78 return Err(Error::Core(barnabas_core::Error::Codec(
79 "consumer protocol blob is too short for its version".to_owned(),
80 )));
81 }
82 Ok(cursor.get_i16())
83}
84
85#[derive(Debug, Clone)]
95pub struct GroupMetadata {
96 pub(crate) group_id: String,
97 pub(crate) generation_id: i32,
98 pub(crate) member_id: String,
99 pub(crate) group_instance_id: Option<String>,
103}
104
105impl GroupMetadata {
106 #[must_use]
109 pub fn group_id(&self) -> &str {
110 &self.group_id
111 }
112}
113
114pub trait GroupProtocol<T: Transport> {
120 fn advance(
123 &mut self,
124 cluster: &mut Cluster<T>,
125 ) -> impl std::future::Future<Output = Result<Membership>>;
126
127 fn leave(&mut self, cluster: &mut Cluster<T>) -> impl std::future::Future<Output = Result<()>>;
130
131 fn commit(
143 &mut self,
144 cluster: &mut Cluster<T>,
145 offsets: &BTreeMap<TopicPartition, i64>,
146 ) -> impl std::future::Future<Output = Result<()>>;
147
148 fn topics(&self) -> Vec<String>;
151
152 fn request_rejoin(&mut self);
159
160 fn group_metadata(&self) -> Option<GroupMetadata>;
165
166 fn committed(
172 &mut self,
173 cluster: &mut Cluster<T>,
174 partitions: &[TopicPartition],
175 ) -> impl std::future::Future<Output = Result<BTreeMap<TopicPartition, i64>>>;
176}
177
178#[derive(Debug, Clone, PartialEq, Eq)]
180pub enum Membership {
181 InProgress,
183 Assigned(Vec<TopicPartition>),
185 Revoked(Vec<TopicPartition>),
192}
193
194pub struct ClassicProtocol {
196 member: GroupMember,
197 assignor: Box<dyn Assignor>,
198 coordinator: Option<String>,
199 session_timeout_ms: i32,
200 rebalance_timeout_ms: i32,
201 announced_revoked: bool,
203 ever_assigned: bool,
207}
208
209impl ClassicProtocol {
210 #[must_use]
211 pub fn new(
212 group_id: impl Into<String>,
213 topics: Vec<String>,
214 assignor: Box<dyn Assignor>,
215 ) -> Self {
216 let protocol = if assignor.name() == "cooperative-sticky" {
221 barnabas_core::member::RebalanceProtocol::Cooperative
222 } else {
223 barnabas_core::member::RebalanceProtocol::Eager
224 };
225 Self {
226 member: GroupMember::new(group_id, topics).with_protocol(protocol),
227 assignor,
228 coordinator: None,
229 session_timeout_ms: 45_000,
230 rebalance_timeout_ms: 300_000,
231 announced_revoked: false,
232 ever_assigned: false,
233 }
234 }
235
236 #[must_use]
237 pub fn member(&self) -> &GroupMember {
238 &self.member
239 }
240
241 pub fn set_session_timeout(&mut self, ms: i32) {
243 self.session_timeout_ms = ms;
244 }
245
246 pub fn set_rebalance_timeout(&mut self, ms: i32) {
250 self.rebalance_timeout_ms = ms;
251 }
252
253 async fn coordinator_addr<T: Transport>(&mut self, cluster: &mut Cluster<T>) -> Result<String> {
256 if let Some(addr) = &self.coordinator {
257 return Ok(addr.clone());
258 }
259 let mut req = FindCoordinatorRequest::default();
260 req.key = StrBytes::from_string(self.member.group_id().to_owned());
261 req.key_type = 0; for attempt in 0..COORDINATOR_RETRIES {
268 let resp: FindCoordinatorResponse =
269 cluster.call_any(ApiKey::FindCoordinator, 3, &req).await?;
270 match resp.error_code {
271 codes::NONE => {
272 let addr = format!("{}:{}", resp.host.as_str(), resp.port);
273 self.coordinator = Some(addr.clone());
274 return Ok(addr);
275 }
276 codes::COORDINATOR_NOT_AVAILABLE | codes::COORDINATOR_LOAD_IN_PROGRESS => {
277 let _ = attempt;
278 T::sleep(COORDINATOR_BACKOFF).await;
279 }
280 code => crate::check("FindCoordinator", code)?,
281 }
282 }
283 Err(Error::Broker {
284 op: "FindCoordinator",
285 code: codes::COORDINATOR_NOT_AVAILABLE,
286 disposition: barnabas_core::Disposition::Retry,
287 })
288 }
289
290 async fn join<T: Transport>(&mut self, cluster: &mut Cluster<T>) -> Result<Step> {
291 let addr = self.coordinator_addr(cluster).await?;
292
293 let mut protocol = JoinGroupRequestProtocol::default();
294 protocol.name = StrBytes::from_string(self.assignor.name().to_owned());
295 protocol.metadata = encode_subscription(&self.member.subscription())?;
296
297 let mut req = JoinGroupRequest::default();
298 req.group_id = GroupId(StrBytes::from_string(self.member.group_id().to_owned()));
299 req.session_timeout_ms = self.session_timeout_ms;
300 req.rebalance_timeout_ms = self.rebalance_timeout_ms;
301 req.member_id = StrBytes::from_string(self.member.member_id().to_owned());
302 req.protocol_type = StrBytes::from_static_str(CONSUMER_PROTOCOL);
303 req.protocols = vec![protocol];
304
305 let deadline =
308 Duration::from_millis(u64::try_from(self.rebalance_timeout_ms).unwrap_or(300_000))
309 + JOIN_SLACK;
310 let resp: JoinGroupResponse = cluster
311 .call_coordinator(&addr, ApiKey::JoinGroup, 7, &req, deadline)
312 .await?;
313
314 let members = if resp.leader == resp.member_id {
317 resp.members
318 .iter()
319 .map(|m| decode_subscription(m.member_id.as_str(), &m.metadata))
320 .collect::<Result<Vec<_>>>()?
321 } else {
322 Vec::new()
323 };
324
325 if std::env::var("BARNABAS_TRACE").is_ok() {
326 eprintln!(
327 "[{}] JOIN<- err={} gen={} id={} leader={}",
328 self.member.member_id(),
329 resp.error_code,
330 resp.generation_id,
331 resp.member_id.as_str(),
332 resp.leader.as_str()
333 );
334 }
335 Ok(self.member.on_join(
336 resp.error_code,
337 resp.generation_id,
338 resp.member_id.as_str(),
339 resp.leader.as_str(),
340 members,
341 ))
342 }
343
344 async fn sync<T: Transport>(
345 &mut self,
346 cluster: &mut Cluster<T>,
347 assignments: Vec<SyncGroupRequestAssignment>,
348 ) -> Result<Step> {
349 let addr = self.coordinator_addr(cluster).await?;
350
351 if std::env::var("BARNABAS_TRACE").is_ok() {
352 let sub = self.member.subscription();
353 eprintln!(
354 "[{}] SYNC-> gen={} owned={:?} sending={:?}",
355 self.member.member_id(),
356 self.member.generation(),
357 sub.owned.iter().map(|t| t.partition).collect::<Vec<_>>(),
358 assignments
359 .iter()
360 .map(|a| (a.member_id.to_string(), a.assignment.len()))
361 .collect::<Vec<_>>()
362 );
363 }
364 let mut req = SyncGroupRequest::default();
365 req.group_id = GroupId(StrBytes::from_string(self.member.group_id().to_owned()));
366 req.generation_id = self.member.generation();
367 req.member_id = StrBytes::from_string(self.member.member_id().to_owned());
368 req.protocol_type = Some(StrBytes::from_static_str(CONSUMER_PROTOCOL));
369 req.protocol_name = Some(StrBytes::from_string(self.assignor.name().to_owned()));
370 req.assignments = assignments;
371
372 let resp: SyncGroupResponse = cluster
373 .call_coordinator(&addr, ApiKey::SyncGroup, 4, &req, COORDINATOR_TIMEOUT)
374 .await?;
375 let assigned = if resp.error_code == codes::NONE && !resp.assignment.is_empty() {
376 decode_assignment(&resp.assignment)?
377 } else {
378 Vec::new()
379 };
380 if std::env::var("BARNABAS_TRACE").is_ok() {
381 eprintln!(
382 "[{}] SYNC<- err={} assigned={:?}",
383 self.member.member_id(),
384 resp.error_code,
385 assigned.iter().map(|t| t.partition).collect::<Vec<_>>()
386 );
387 }
388 let step = self.member.on_sync(resp.error_code, assigned);
389 if std::env::var("BARNABAS_TRACE").is_ok() {
390 eprintln!(
391 "[{}] after: gen={} assignment={:?} lost={:?} step={:?}",
392 self.member.member_id(),
393 self.member.generation(),
394 self.member
395 .assignment()
396 .iter()
397 .map(|t| t.partition)
398 .collect::<Vec<_>>(),
399 self.member
400 .lost()
401 .iter()
402 .map(|t| t.partition)
403 .collect::<Vec<_>>(),
404 step
405 );
406 }
407 Ok(step)
408 }
409
410 async fn heartbeat<T: Transport>(&mut self, cluster: &mut Cluster<T>) -> Result<Step> {
411 let addr = self.coordinator_addr(cluster).await?;
412
413 let mut req = HeartbeatRequest::default();
414 req.group_id = GroupId(StrBytes::from_string(self.member.group_id().to_owned()));
415 req.generation_id = self.member.generation();
416 req.member_id = StrBytes::from_string(self.member.member_id().to_owned());
417
418 let resp: HeartbeatResponse = cluster
419 .call_coordinator(&addr, ApiKey::Heartbeat, 4, &req, COORDINATOR_TIMEOUT)
420 .await?;
421 if std::env::var("BARNABAS_TRACE").is_ok() {
422 eprintln!(
423 "[{}] HB<- err={} gen={}",
424 self.member.member_id(),
425 resp.error_code,
426 self.member.generation()
427 );
428 }
429 Ok(self.member.on_heartbeat(resp.error_code))
430 }
431
432 async fn assign_and_sync<T: Transport>(
434 &mut self,
435 cluster: &mut Cluster<T>,
436 members: Vec<Subscription>,
437 ) -> Result<Step> {
438 let mut partitions_per_topic: BTreeMap<String, i32> = BTreeMap::new();
441 for topic in members.iter().flat_map(|m| m.topics.iter()) {
442 if !partitions_per_topic.contains_key(topic) {
443 let count = cluster.partition_count(topic).await?;
444 partitions_per_topic.insert(topic.clone(), count);
445 }
446 }
447
448 if std::env::var("BARNABAS_TRACE").is_ok() {
449 eprintln!(
450 "[{}] LEADER sees: {:?}",
451 self.member.member_id(),
452 members
453 .iter()
454 .map(|m| (
455 m.member_id.clone(),
456 m.generation,
457 m.owned.iter().map(|t| t.partition).collect::<Vec<_>>()
458 ))
459 .collect::<Vec<_>>()
460 );
461 }
462 let assignment = self.assignor.assign(&members, &partitions_per_topic);
463 if std::env::var("BARNABAS_TRACE").is_ok() {
464 eprintln!(
465 "[{}] LEADER assigns: {:?}",
466 self.member.member_id(),
467 assignment
468 .iter()
469 .map(|(m, p)| (m.clone(), p.iter().map(|t| t.partition).collect::<Vec<_>>()))
470 .collect::<Vec<_>>()
471 );
472 }
473 let encoded = assignment
474 .iter()
475 .map(|(member_id, partitions)| {
476 let mut entry = SyncGroupRequestAssignment::default();
477 entry.member_id = StrBytes::from_string(member_id.clone());
478 entry.assignment = encode_assignment(partitions)?;
479 Ok(entry)
480 })
481 .collect::<Result<Vec<_>>>()?;
482
483 self.sync(cluster, encoded).await
484 }
485}
486
487impl<T: Transport> GroupProtocol<T> for ClassicProtocol {
488 async fn advance(&mut self, cluster: &mut Cluster<T>) -> Result<Membership> {
489 let step = self.member.step();
490 let outcome = match step {
491 Step::Join { .. } => self.join(cluster).await?,
492 Step::AssignAndSync { members } => self.assign_and_sync(cluster, members).await?,
493 Step::Sync => self.sync(cluster, Vec::new()).await?,
494 Step::Heartbeat => self.heartbeat(cluster).await?,
495 Step::FindCoordinator => {
496 self.coordinator = None;
497 Step::Join {
498 member_id: self.member.member_id().to_owned(),
499 }
500 }
501 };
502
503 if outcome == Step::FindCoordinator {
506 self.coordinator = None;
507 }
508
509 if self.member.state() == barnabas_core::member::MemberState::Stable {
510 self.announced_revoked = false;
511 self.ever_assigned = true;
512 return Ok(Membership::Assigned(self.member.assignment().to_vec()));
513 }
514 if self.ever_assigned && !self.announced_revoked {
518 self.announced_revoked = true;
519 return Ok(Membership::Revoked(self.member.lost().to_vec()));
520 }
521 Ok(Membership::InProgress)
522 }
523
524 async fn commit(
525 &mut self,
526 cluster: &mut Cluster<T>,
527 offsets: &BTreeMap<TopicPartition, i64>,
528 ) -> Result<()> {
529 self.commit_offsets(cluster, offsets).await
530 }
531
532 fn topics(&self) -> Vec<String> {
533 self.member.topics().to_vec()
534 }
535
536 fn request_rejoin(&mut self) {
537 self.member.request_rejoin();
538 }
539
540 fn group_metadata(&self) -> Option<GroupMetadata> {
541 if !self.member.can_commit() {
545 return None;
546 }
547 Some(GroupMetadata {
548 group_id: self.member.group_id().to_owned(),
549 generation_id: self.member.generation(),
550 member_id: self.member.member_id().to_owned(),
551 group_instance_id: None,
552 })
553 }
554
555 async fn committed(
556 &mut self,
557 cluster: &mut Cluster<T>,
558 partitions: &[TopicPartition],
559 ) -> Result<BTreeMap<TopicPartition, i64>> {
560 self.fetch_offsets(cluster, partitions).await
561 }
562
563 async fn leave(&mut self, cluster: &mut Cluster<T>) -> Result<()> {
564 if self.member.member_id().is_empty() {
565 return Ok(());
566 }
567 let addr = self.coordinator_addr(cluster).await?;
568
569 let mut req = LeaveGroupRequest::default();
570 req.group_id = GroupId(StrBytes::from_string(self.member.group_id().to_owned()));
571 req.member_id = StrBytes::from_string(self.member.member_id().to_owned());
572
573 let _: std::result::Result<LeaveGroupResponse, Error> = cluster
576 .call_coordinator(&addr, ApiKey::LeaveGroup, 3, &req, COORDINATOR_TIMEOUT)
577 .await;
578 self.member.on_leave();
579 Ok(())
580 }
581}
582
583impl ClassicProtocol {
584 async fn commit_offsets<T: Transport>(
585 &mut self,
586 cluster: &mut Cluster<T>,
587 offsets: &BTreeMap<TopicPartition, i64>,
588 ) -> Result<()> {
589 if offsets.is_empty() {
590 return Ok(());
591 }
592 if !self.member.can_commit() {
597 return Err(Error::Broker {
598 op: "OffsetCommit",
599 code: codes::REBALANCE_IN_PROGRESS,
600 disposition: barnabas_core::Disposition::Retry,
601 });
602 }
603 let addr = self.coordinator_addr(cluster).await?;
604
605 let mut by_topic: BTreeMap<String, Vec<OffsetCommitRequestPartition>> = BTreeMap::new();
606 for (tp, offset) in offsets {
607 let mut partition = OffsetCommitRequestPartition::default();
608 partition.partition_index = tp.partition;
609 partition.committed_offset = *offset;
610 partition.committed_leader_epoch = -1;
611 by_topic
612 .entry(tp.topic.clone())
613 .or_default()
614 .push(partition);
615 }
616
617 let mut req = OffsetCommitRequest::default();
618 req.group_id = GroupId(StrBytes::from_string(self.member.group_id().to_owned()));
619 req.generation_id_or_member_epoch = self.member.generation();
620 req.member_id = StrBytes::from_string(self.member.member_id().to_owned());
621 req.topics = by_topic
622 .into_iter()
623 .map(|(name, partitions)| {
624 let mut topic = OffsetCommitRequestTopic::default();
625 topic.name = TopicName(StrBytes::from_string(name));
626 topic.partitions = partitions;
627 topic
628 })
629 .collect();
630
631 let resp: OffsetCommitResponse = cluster
632 .call_coordinator(&addr, ApiKey::OffsetCommit, 8, &req, COORDINATOR_TIMEOUT)
633 .await?;
634 for topic in &resp.topics {
635 for partition in &topic.partitions {
636 crate::check("OffsetCommit", partition.error_code)?;
637 }
638 }
639 Ok(())
640 }
641
642 async fn fetch_offsets<T: Transport>(
643 &mut self,
644 cluster: &mut Cluster<T>,
645 partitions: &[TopicPartition],
646 ) -> Result<BTreeMap<TopicPartition, i64>> {
647 if partitions.is_empty() {
648 return Ok(BTreeMap::new());
649 }
650 let addr = self.coordinator_addr(cluster).await?;
651
652 let mut by_topic: BTreeMap<String, Vec<i32>> = BTreeMap::new();
653 for tp in partitions {
654 by_topic
655 .entry(tp.topic.clone())
656 .or_default()
657 .push(tp.partition);
658 }
659
660 let mut req = OffsetFetchRequest::default();
661 req.group_id = GroupId(StrBytes::from_string(self.member.group_id().to_owned()));
662 req.topics = Some(
663 by_topic
664 .into_iter()
665 .map(|(name, partition_indexes)| {
666 let mut topic = OffsetFetchRequestTopic::default();
667 topic.name = TopicName(StrBytes::from_string(name));
668 topic.partition_indexes = partition_indexes;
669 topic
670 })
671 .collect(),
672 );
673
674 let resp: OffsetFetchResponse = cluster
675 .call_coordinator(&addr, ApiKey::OffsetFetch, 6, &req, COORDINATOR_TIMEOUT)
676 .await?;
677 crate::check("OffsetFetch", resp.error_code)?;
678
679 let mut out = BTreeMap::new();
680 for topic in &resp.topics {
681 for partition in &topic.partitions {
682 crate::check("OffsetFetch partition", partition.error_code)?;
683 if partition.committed_offset >= 0 {
687 out.insert(
688 TopicPartition::new(topic.name.0.to_string(), partition.partition_index),
689 partition.committed_offset,
690 );
691 }
692 }
693 }
694 Ok(out)
695 }
696}
697
698fn encode_subscription(subscription: &Subscription) -> Result<Bytes> {
701 let mut owned: BTreeMap<String, Vec<i32>> = BTreeMap::new();
702 for tp in &subscription.owned {
703 owned
704 .entry(tp.topic.clone())
705 .or_default()
706 .push(tp.partition);
707 }
708
709 let mut body = ConsumerProtocolSubscription::default();
710 body.topics = subscription
711 .topics
712 .iter()
713 .map(|t| StrBytes::from_string(t.clone()))
714 .collect();
715 body.generation_id = subscription.generation;
716 body.owned_partitions = owned
717 .into_iter()
718 .map(|(topic, partitions)| {
719 let mut entry = SubscriptionTopicPartition::default();
720 entry.topic = TopicName(StrBytes::from_string(topic));
721 entry.partitions = partitions;
722 entry
723 })
724 .collect();
725
726 let mut buf = bytes::BytesMut::new();
732 buf.extend_from_slice(&PROTOCOL_VERSION.to_be_bytes());
733 body.encode(&mut buf, PROTOCOL_VERSION)
734 .map_err(|e| Error::Core(barnabas_core::Error::Codec(format!("subscription: {e}"))))?;
735 Ok(buf.freeze())
736}
737
738fn decode_subscription(member_id: &str, metadata: &Bytes) -> Result<Subscription> {
739 let mut cursor = metadata.clone();
740 let version = read_version(&mut cursor)?;
741 let body = ConsumerProtocolSubscription::decode(&mut cursor, version)
742 .map_err(|e| Error::Core(barnabas_core::Error::Codec(format!("subscription: {e}"))))?;
743
744 Ok(Subscription {
745 member_id: member_id.to_owned(),
746 generation: if version >= 2 { body.generation_id } else { -1 },
749 topics: body.topics.iter().map(|t| t.to_string()).collect(),
750 owned: body
751 .owned_partitions
752 .iter()
753 .flat_map(|tp| {
754 let topic = tp.topic.0.to_string();
755 tp.partitions
756 .iter()
757 .map(move |p| TopicPartition::new(topic.clone(), *p))
758 })
759 .collect(),
760 })
761}
762
763fn encode_assignment(partitions: &[TopicPartition]) -> Result<Bytes> {
764 let mut by_topic: BTreeMap<String, Vec<i32>> = BTreeMap::new();
765 for tp in partitions {
766 by_topic
767 .entry(tp.topic.clone())
768 .or_default()
769 .push(tp.partition);
770 }
771
772 let mut body = ConsumerProtocolAssignment::default();
773 body.assigned_partitions = by_topic
774 .into_iter()
775 .map(|(topic, partitions)| {
776 let mut entry = AssignmentTopicPartition::default();
777 entry.topic = TopicName(StrBytes::from_string(topic));
778 entry.partitions = partitions;
779 entry
780 })
781 .collect();
782
783 let mut buf = bytes::BytesMut::new();
784 buf.extend_from_slice(&PROTOCOL_VERSION.to_be_bytes());
785 body.encode(&mut buf, PROTOCOL_VERSION)
786 .map_err(|e| Error::Core(barnabas_core::Error::Codec(format!("assignment: {e}"))))?;
787 Ok(buf.freeze())
788}
789
790fn decode_assignment(assignment: &Bytes) -> Result<Vec<TopicPartition>> {
791 let mut cursor = assignment.clone();
792 let version = read_version(&mut cursor)?;
793 let body = ConsumerProtocolAssignment::decode(&mut cursor, version)
794 .map_err(|e| Error::Core(barnabas_core::Error::Codec(format!("assignment: {e}"))))?;
795
796 let mut out: Vec<TopicPartition> = body
797 .assigned_partitions
798 .iter()
799 .flat_map(|tp| {
800 let topic = tp.topic.0.to_string();
801 tp.partitions
802 .iter()
803 .map(move |p| TopicPartition::new(topic.clone(), *p))
804 })
805 .collect();
806 out.sort();
807 Ok(out)
808}
809
810#[cfg(test)]
811mod tests {
812 use super::*;
813
814 #[test]
816 fn a_subscription_round_trips() {
817 let subscription = Subscription {
818 member_id: "m-1".to_owned(),
819 topics: vec!["a".to_owned(), "b".to_owned()],
820 owned: vec![TopicPartition::new("a", 0), TopicPartition::new("a", 3)],
821 generation: 7,
822 };
823 let encoded = encode_subscription(&subscription).expect("encode");
824 let decoded = decode_subscription("m-1", &encoded).expect("decode");
825 assert_eq!(decoded.topics, subscription.topics);
826 assert_eq!(decoded.owned, subscription.owned);
827 assert_eq!(
828 decoded.generation, 7,
829 "the generation must survive: without it a leader cannot tell a \
830 live ownership claim from a stale one"
831 );
832 }
833
834 #[test]
835 fn an_assignment_round_trips() {
836 let partitions = vec![
837 TopicPartition::new("a", 0),
838 TopicPartition::new("a", 1),
839 TopicPartition::new("b", 7),
840 ];
841 let encoded = encode_assignment(&partitions).expect("encode");
842 assert_eq!(decode_assignment(&encoded).expect("decode"), partitions);
843 }
844
845 #[test]
848 fn an_empty_assignment_decodes_to_nothing() {
849 let encoded = encode_assignment(&[]).expect("encode");
850 assert!(decode_assignment(&encoded).expect("decode").is_empty());
851 }
852}