1use std::{
68 collections::HashMap,
69 io,
70 net::SocketAddr,
71 sync::Arc,
72 time::{SystemTime, UNIX_EPOCH},
73};
74
75use async_trait::async_trait;
76use krabka_client_admin::{
77 AdminClient, AdminClientLike as _, AdminError, CreateTopicOutcome, CreateTopicSpec,
78 TopicMutationOptions, TransactionDescription,
79};
80use krabka_client_core::{
81 BrokerInfo, BrokerPool, ClientDnsTimeout, ClientError, ClientSecurity, Connection,
82 ConnectionOptions, DEFAULT_FETCH_RESPONSE_MAX, FetchMinBytes, FetchedRecord, IsolatedFetch,
83 connection_target_host, fetch_partition_with_isolation_progress, transport::resolve,
84};
85use krabka_client_producer::{
86 Acks, Producer, ProducerError, ProducerRecord, RecordMetadata, partition_for_key,
87};
88use krabka_protocol::{
89 owned::{
90 list_offsets_request::{ListOffsetsPartition, ListOffsetsRequest, ListOffsetsTopic},
91 list_offsets_response::ListOffsetsResponse,
92 metadata_request::{MetadataRequest, MetadataRequestTopic},
93 metadata_response,
94 metadata_response::MetadataResponse,
95 },
96 primitives::uuid::Uuid as WireUuid,
97};
98use krabka_units::{ByteSize, Time, convert::TimeExt as _, mebibytes, millis, secs};
99use tokio::{
100 sync::{Mutex, OnceCell},
101 time::{error::Elapsed, timeout},
102};
103
104use crate::{
105 error::CoordinationError,
106 lease::DEFAULT_LEASE_DURATION,
107 record::{
108 COORDINATION_STATE_TOPIC, CoordinationKey, CoordinationRecord, FencingToken, Lease,
109 MemberId, Registration, Role, decode_key, decode_value, encode_key, encode_value,
110 },
111 transport::{CoordinationTransport, RoleRecords},
112};
113
114pub const DEFAULT_COORDINATION_PARTITIONS: i32 = 16;
117
118pub const DEFAULT_COORDINATION_REPLICATION: i32 = 3;
126
127pub const DEFAULT_COORDINATION_REQUEST_TIMEOUT: Time = secs(30);
129
130pub const DEFAULT_COORDINATION_FETCH_MAX_WAIT: Time = millis(500);
132
133pub const DEFAULT_COORDINATION_FETCH_PARTITION_MAX: ByteSize = mebibytes(1);
135
136pub const DEFAULT_COORDINATION_CLIENT_ID: &str = "krabka-coordination";
138
139const UNKNOWN_TOPIC_OR_PARTITION: i16 = 3;
141
142const LEADER_NOT_AVAILABLE: i16 = 5;
144
145const TOPIC_ALREADY_EXISTS: i16 = 36;
148
149const INVALID_PRODUCER_EPOCH: i16 = 47;
152
153const PRODUCER_FENCED: i16 = 90;
156
157const TRANSACTIONAL_ID_NOT_FOUND: i16 = 105;
160
161const READ_COMMITTED: i8 = 1;
163
164const LIST_OFFSETS_EARLIEST: i64 = -2;
166
167const LIST_OFFSETS_LATEST: i64 = -1;
170
171#[derive(Debug, Clone, Copy, PartialEq, Eq)]
178struct TopicLayout {
179 partitions: i32,
181 topic_id: WireUuid,
183}
184
185struct Holder {
187 producer: Arc<Producer>,
189 token: FencingToken,
191}
192
193pub struct BrokerTransport {
202 bootstrap: String,
204 client_id: String,
206 security: Option<ClientSecurity>,
208 lease_duration: Time,
210 request_timeout: Time,
212 fetch_max_wait: Time,
214 fetch_partition_max: ByteSize,
216 fetch_min: FetchMinBytes,
218 topic_partitions: i32,
220 topic_replication: i32,
222 admin: Mutex<AdminClient>,
225 pool: BrokerPool,
229 layout: OnceCell<TopicLayout>,
231 registrar: OnceCell<Producer>,
233 holders: Mutex<HashMap<Role, Holder>>,
236}
237
238#[bon::bon]
239impl BrokerTransport {
240 #[builder(start_fn = builder, finish_fn = build)]
259 #[tracing::instrument(
260 level = "info",
261 skip_all,
262 fields(bootstrap = %bootstrap, client_id = %client_id),
263 err(level = "info"),
264 )]
265 pub async fn start(
266 #[builder(into)] bootstrap: String,
267 #[builder(into, default = DEFAULT_COORDINATION_CLIENT_ID.to_owned())] client_id: String,
268 #[builder(default = DEFAULT_LEASE_DURATION)] lease_duration: Time,
269 #[builder(default = DEFAULT_COORDINATION_REQUEST_TIMEOUT)] request_timeout: Time,
270 #[builder(default = DEFAULT_COORDINATION_FETCH_MAX_WAIT)] fetch_max_wait: Time,
271 #[builder(default = DEFAULT_COORDINATION_FETCH_PARTITION_MAX)]
272 fetch_partition_max: ByteSize,
273 #[builder(default = DEFAULT_COORDINATION_PARTITIONS)] topic_partitions: i32,
274 #[builder(default = DEFAULT_COORDINATION_REPLICATION)] topic_replication: i32,
275 security: Option<ClientSecurity>,
276 ) -> Result<Self, CoordinationError> {
277 check_topic_shape(topic_partitions, topic_replication)?;
278 let addresses = bootstrap_addresses(&bootstrap);
279 if addresses.is_empty() {
280 return Err(CoordinationError::InvalidConfig(format!(
281 "the bootstrap list {bootstrap:?} holds no address"
282 )));
283 }
284 let fetch_min = FetchMinBytes::new(1).map_err(CoordinationError::InvalidConfig)?;
285 let options = ConnectionOptions {
286 client_id: client_id.clone(),
287 request_timeout,
288 security: security.clone().map(Box::new),
289 ..ConnectionOptions::default()
290 };
291 let resolved = resolve_addresses(&addresses, options.dns_timeout).await?;
292 let admin = AdminClient::connect_secured(&addresses, security.clone()).await?;
293 Ok(Self {
294 bootstrap,
295 client_id,
296 security,
297 lease_duration,
298 request_timeout,
299 fetch_max_wait,
300 fetch_partition_max,
301 fetch_min,
302 topic_partitions,
303 topic_replication,
304 admin: Mutex::new(admin),
305 pool: BrokerPool::new_with_server_names(resolved, options),
306 layout: OnceCell::new(),
307 registrar: OnceCell::new(),
308 holders: Mutex::new(HashMap::new()),
309 })
310 }
311}
312
313impl BrokerTransport {
314 pub async fn bound_producer(&self, role: &Role) -> Option<Arc<Producer>> {
321 let holders = self.holders.lock().await;
322 holders.get(role).map(|holder| Arc::clone(&holder.producer))
323 }
324
325 pub async fn held_token(&self, role: &Role) -> Option<FencingToken> {
332 let holders = self.holders.lock().await;
333 holders.get(role).map(|holder| holder.token)
334 }
335
336 pub async fn close(self) {
342 for (role, holder) in self.holders.into_inner() {
343 close_producer(&role, holder.producer).await;
344 }
345 let closed = match self.registrar.into_inner() {
346 Some(registrar) => registrar.close().await,
347 None => Ok(()),
348 };
349 if let Err(error) = closed {
350 tracing::warn!(%error, "the registration producer did not close cleanly");
351 }
352 self.pool.close_all();
353 }
354
355 async fn partition_of(&self, role: &Role) -> Result<i32, CoordinationError> {
360 let layout = self.topic_layout().await?;
361 role_partition(role, layout.partitions)
362 }
363
364 async fn topic_layout(&self) -> Result<TopicLayout, CoordinationError> {
366 self.layout
367 .get_or_try_init(|| Box::pin(self.resolve_topic()))
368 .await
369 .copied()
370 }
371
372 async fn resolve_topic(&self) -> Result<TopicLayout, CoordinationError> {
374 self.create_topic().await?;
375 let metadata = self.read_metadata().await?;
376 topic_layout(&metadata)
377 }
378
379 #[tracing::instrument(level = "info", skip_all, err(level = "info"))]
382 async fn create_topic(&self) -> Result<(), CoordinationError> {
383 let spec = coordination_topic_spec(self.topic_partitions, self.topic_replication);
384 let outcomes = self
385 .admin
386 .lock()
387 .await
388 .create_topics(
389 &[spec],
390 TopicMutationOptions::with_timeout(self.request_timeout),
391 )
392 .await?;
393 accept_topic_creation(&outcomes)
394 }
395
396 async fn read_metadata(&self) -> Result<MetadataResponse, CoordinationError> {
398 let connection = self.pool.bootstrap_connection().await?;
399 let metadata = connection.send(coordination_metadata_request()).await?;
400 Ok(metadata)
401 }
402
403 async fn leader_connection(
405 &self,
406 partition: i32,
407 ) -> Result<Arc<Connection>, CoordinationError> {
408 let metadata = self.read_metadata().await?;
409 self.pool.refresh_brokers(&brokers_of(&metadata)).await;
410 let leader = partition_leader(&metadata, partition)?;
411 Ok(self.pool.get(leader).await?)
412 }
413
414 async fn list_offset(
416 &self,
417 connection: &Connection,
418 partition: i32,
419 timestamp: i64,
420 ) -> Result<i64, CoordinationError> {
421 let response = connection
422 .send(list_offsets_request(partition, timestamp))
423 .await?;
424 list_offsets_offset(&response, partition)
425 }
426
427 async fn hold_role(
434 &self,
435 role: &Role,
436 producer: Producer,
437 ) -> Result<FencingToken, CoordinationError> {
438 producer
439 .init_transactions()
440 .await
441 .map_err(|error| fenced_or(role, error))?;
442 let (producer_id, producer_epoch) =
447 producer
448 .transactional_identity()
449 .await
450 .ok_or(CoordinationError::Producer(
451 ProducerError::InvalidTransactionState(
452 "init_transactions returned without an identity",
453 ),
454 ))?;
455 let token = FencingToken::new(producer_id, producer_epoch)?;
456 self.install_holder(
457 role,
458 Holder {
459 producer: Arc::new(producer),
460 token,
461 },
462 )
463 .await;
464 Ok(token)
465 }
466
467 async fn install_holder(&self, role: &Role, holder: Holder) {
470 let replaced = self.holders.lock().await.insert(role.clone(), holder);
471 if let Some(replaced) = replaced {
472 close_producer(role, replaced.producer).await;
473 }
474 }
475
476 async fn registrar(&self) -> Result<&Producer, CoordinationError> {
478 self.registrar
479 .get_or_try_init(|| Box::pin(self.build_registrar()))
480 .await
481 }
482
483 async fn build_registrar(&self) -> Result<Producer, CoordinationError> {
486 let producer = Producer::builder()
487 .bootstrap(self.bootstrap.clone())
488 .client_id(self.client_id.clone())
489 .acks(Acks::All)
490 .request_timeout(self.request_timeout.to_std())
491 .maybe_security(self.security.clone())
492 .build()
493 .await?;
494 Ok(producer)
495 }
496
497 async fn build_role_producer(&self, role: &Role) -> Result<Producer, CoordinationError> {
500 let producer = Producer::builder()
501 .bootstrap(self.bootstrap.clone())
502 .client_id(self.client_id.clone())
503 .acks(Acks::All)
504 .request_timeout(self.request_timeout.to_std())
505 .transactional_id(role.as_str().to_owned())
506 .transaction_timeout(self.lease_duration.to_std())
507 .maybe_security(self.security.clone())
508 .build()
509 .await
510 .map_err(|error| fenced_or(role, error))?;
511 Ok(producer)
512 }
513
514 async fn holder_of(&self, role: &Role) -> Option<(Arc<Producer>, FencingToken)> {
517 let holders = self.holders.lock().await;
518 holders
519 .get(role)
520 .map(|holder| (Arc::clone(&holder.producer), holder.token))
521 }
522}
523
524#[async_trait]
525impl CoordinationTransport for BrokerTransport {
526 #[tracing::instrument(level = "info", skip_all, fields(role = %role), err(level = "info"))]
548 async fn acquire_epoch(&self, role: &Role) -> Result<FencingToken, CoordinationError> {
549 self.topic_layout().await?;
550 let producer = self.build_role_producer(role).await?;
551 self.hold_role(role, producer).await
552 }
553
554 #[tracing::instrument(level = "debug", skip_all, fields(role = %role), err(level = "debug"))]
575 async fn read_role_records(&self, role: &Role) -> Result<RoleRecords, CoordinationError> {
576 let layout = self.topic_layout().await?;
577 let partition = role_partition(role, layout.partitions)?;
578 let connection = self.leader_connection(partition).await?;
579 let start = self
580 .list_offset(&connection, partition, LIST_OFFSETS_EARLIEST)
581 .await?;
582 let end = self
583 .list_offset(&connection, partition, LIST_OFFSETS_LATEST)
584 .await?;
585
586 let mut offset = start;
587 let mut records = RoleRecords::new();
588 while offset < end {
589 let page = fetch_partition_with_isolation_progress(
590 &connection,
591 IsolatedFetch {
592 topic: COORDINATION_STATE_TOPIC,
593 topic_id: layout.topic_id,
594 partition,
595 fetch_offset: offset,
596 max_wait: self.fetch_max_wait,
597 max: DEFAULT_FETCH_RESPONSE_MAX,
598 partition_max: self.fetch_partition_max,
599 fetch_min: self.fetch_min,
600 isolation_level: READ_COMMITTED,
601 },
602 )
603 .await?;
604 records.append(&mut select_role_records(role, &page.records)?);
605 match next_scan_offset(offset, end, page.next_offset) {
606 Some(next) => offset = next,
607 None => break,
608 }
609 }
610 Ok(records)
611 }
612
613 #[tracing::instrument(level = "info", skip_all, fields(role = %role, member = %member), err(level = "info"))]
626 async fn register(&self, role: &Role, member: &MemberId) -> Result<(), CoordinationError> {
627 let partition = self.partition_of(role).await?;
628 send_one(
629 self.registrar().await?,
630 registration_record(role, member, partition, current_millis()),
631 )
632 .await?;
633 Ok(())
634 }
635
636 #[tracing::instrument(
654 level = "info",
655 skip_all,
656 fields(role = %role, token = %token, member = %lease.member),
657 err(level = "info"),
658 )]
659 async fn write_lease(
660 &self,
661 role: &Role,
662 token: FencingToken,
663 lease: &Lease,
664 ) -> Result<(), CoordinationError> {
665 let partition = self.partition_of(role).await?;
666 let bound = self
667 .holder_of(role)
668 .await
669 .filter(|(_producer, held)| *held == token);
670 let Some((producer, _held)) = bound else {
671 return Err(CoordinationError::NotHeld {
672 role: role.clone(),
673 member: lease.member.clone(),
674 });
675 };
676
677 let transaction = producer
678 .begin_transaction()
679 .await
680 .map_err(|error| fenced_or(role, error))?;
681 if let Err(error) = send_one(&producer, lease_record(role, lease, partition)).await {
682 if let Err(failed) = transaction.abort().await {
683 tracing::warn!(
684 role = %role,
685 error = %failed.source,
686 "the abort of a failed lease write did not complete",
687 );
688 }
689 return Err(fenced_or(role, error));
690 }
691 transaction
692 .commit()
693 .await
694 .map_err(|failed| fenced_or(role, failed.source))
695 }
696
697 #[tracing::instrument(level = "debug", skip_all, fields(role = %role), err(level = "debug"))]
709 async fn describe(&self, role: &Role) -> Result<Option<FencingToken>, CoordinationError> {
710 let admin = self.admin.lock().await;
711 match admin.describe_transaction(role.as_str()).await {
712 Ok(description) => Ok(token_from_description(&description)),
713 Err(error) if reports_unknown_transactional_id(&error) => Ok(None),
714 Err(error) => Err(error.into()),
715 }
716 }
717}
718
719fn bootstrap_addresses(bootstrap: &str) -> Vec<String> {
721 bootstrap
722 .split(',')
723 .map(str::trim)
724 .filter(|address| !address.is_empty())
725 .map(ToOwned::to_owned)
726 .collect()
727}
728
729fn check_topic_shape(partitions: i32, replication: i32) -> Result<(), CoordinationError> {
731 if partitions <= 0 {
732 return Err(CoordinationError::InvalidConfig(format!(
733 "the coordination topic needs at least one partition, got {partitions}"
734 )));
735 }
736 if replication <= 0 {
737 return Err(CoordinationError::InvalidConfig(format!(
738 "the coordination topic needs at least one replica, got {replication}"
739 )));
740 }
741 Ok(())
742}
743
744async fn resolve_addresses(
746 addresses: &[String],
747 dns_timeout: ClientDnsTimeout,
748) -> Result<Vec<(SocketAddr, String)>, CoordinationError> {
749 let mut resolved = Vec::new();
750 for address in addresses {
751 let lookup = timeout(dns_timeout.time().to_std(), resolve(address)).await;
752 resolved.extend(
753 lookup_addresses(address, lookup)?
754 .into_iter()
755 .map(|socket| (socket, connection_target_host(address).to_owned())),
756 );
757 }
758 if resolved.is_empty() {
759 return Err(CoordinationError::InvalidConfig(format!(
760 "no address of {addresses:?} resolved"
761 )));
762 }
763 Ok(resolved)
764}
765
766fn lookup_addresses(
769 address: &str,
770 lookup: Result<io::Result<Vec<SocketAddr>>, Elapsed>,
771) -> Result<Vec<SocketAddr>, CoordinationError> {
772 lookup
773 .map_err(|_elapsed| {
774 CoordinationError::InvalidConfig(format!("the DNS lookup of {address} timed out"))
775 })?
776 .map_err(|error| {
777 CoordinationError::InvalidConfig(format!("the DNS lookup of {address} failed: {error}"))
778 })
779}
780
781pub fn role_partition(role: &Role, partitions: i32) -> Result<i32, CoordinationError> {
800 if partitions <= 0 {
801 return Err(CoordinationError::InvalidConfig(format!(
802 "topic {COORDINATION_STATE_TOPIC} reports {partitions} partitions"
803 )));
804 }
805 Ok(partition_for_key(role.as_str().as_bytes(), partitions))
806}
807
808fn coordination_topic_spec(partitions: i32, replication: i32) -> CreateTopicSpec {
813 CreateTopicSpec::builder()
814 .name(COORDINATION_STATE_TOPIC)
815 .partitions(partitions)
816 .replicas(replication)
817 .configs(maplit::btreemap! {"cleanup.policy".to_owned() => "compact".to_owned()})
818 .build()
819}
820
821fn accept_topic_creation(outcomes: &[CreateTopicOutcome]) -> Result<(), CoordinationError> {
830 for outcome in outcomes {
831 match &outcome.error {
832 Some(error) if error.code != TOPIC_ALREADY_EXISTS => {
833 return Err(CoordinationError::Admin(AdminError::Broker {
834 api: "CreateTopics",
835 code: error.code,
836 name: error.name,
837 message: error.message.clone(),
838 }));
839 }
840 _ => {}
841 }
842 }
843 Ok(())
844}
845
846fn coordination_metadata_request() -> MetadataRequest {
852 MetadataRequest {
853 topics: Some(vec![MetadataRequestTopic {
854 name: Some(COORDINATION_STATE_TOPIC.to_owned()),
855 ..Default::default()
856 }]),
857 allow_auto_topic_creation: false,
858 ..Default::default()
859 }
860}
861
862fn topic_layout(response: &MetadataResponse) -> Result<TopicLayout, CoordinationError> {
864 let topic = coordination_topic(response).ok_or_else(|| server(UNKNOWN_TOPIC_OR_PARTITION))?;
865 if topic.error_code != 0 {
866 return Err(server(topic.error_code));
867 }
868 let partitions = i32::try_from(topic.partitions.len()).map_err(|_error| {
869 CoordinationError::InvalidConfig(format!(
870 "topic {COORDINATION_STATE_TOPIC} reports {} partitions",
871 topic.partitions.len()
872 ))
873 })?;
874 if partitions == 0 {
875 return Err(server(UNKNOWN_TOPIC_OR_PARTITION));
876 }
877 Ok(TopicLayout {
878 partitions,
879 topic_id: topic.topic_id,
880 })
881}
882
883fn partition_leader(response: &MetadataResponse, partition: i32) -> Result<i32, CoordinationError> {
885 let topic = coordination_topic(response).ok_or_else(|| server(UNKNOWN_TOPIC_OR_PARTITION))?;
886 let entry = topic
887 .partitions
888 .iter()
889 .find(|entry| entry.partition_index == partition)
890 .ok_or_else(|| server(UNKNOWN_TOPIC_OR_PARTITION))?;
891 if entry.error_code != 0 {
892 return Err(server(entry.error_code));
893 }
894 if entry.leader_id < 0 {
895 return Err(server(LEADER_NOT_AVAILABLE));
896 }
897 Ok(entry.leader_id)
898}
899
900fn coordination_topic(
902 response: &MetadataResponse,
903) -> Option<&metadata_response::MetadataResponseTopic> {
904 response
905 .topics
906 .iter()
907 .find(|topic| topic.name.as_deref() == Some(COORDINATION_STATE_TOPIC))
908}
909
910fn brokers_of(response: &MetadataResponse) -> Vec<BrokerInfo> {
912 response
913 .brokers
914 .iter()
915 .map(|broker| BrokerInfo {
916 id: broker.node_id,
917 host: broker.host.clone(),
918 port: broker.port,
919 rack: broker.rack.clone(),
920 })
921 .collect()
922}
923
924fn list_offsets_request(partition: i32, timestamp: i64) -> ListOffsetsRequest {
926 ListOffsetsRequest {
927 replica_id: -1,
928 isolation_level: READ_COMMITTED,
929 topics: vec![ListOffsetsTopic {
930 name: COORDINATION_STATE_TOPIC.to_owned(),
931 partitions: vec![ListOffsetsPartition {
932 partition_index: partition,
933 current_leader_epoch: -1,
934 timestamp,
935 ..Default::default()
936 }],
937 ..Default::default()
938 }],
939 ..Default::default()
940 }
941}
942
943fn list_offsets_offset(
945 response: &ListOffsetsResponse,
946 partition: i32,
947) -> Result<i64, CoordinationError> {
948 let entry = response
949 .topics
950 .iter()
951 .filter(|topic| topic.name == COORDINATION_STATE_TOPIC)
952 .flat_map(|topic| topic.partitions.iter())
953 .find(|entry| entry.partition_index == partition)
954 .ok_or_else(|| server(UNKNOWN_TOPIC_OR_PARTITION))?;
955 if entry.error_code != 0 {
956 return Err(server(entry.error_code));
957 }
958 Ok(entry.offset)
959}
960
961fn next_scan_offset(current: i64, end: i64, next_offset: Option<i64>) -> Option<i64> {
968 let next = next_offset?;
969 if next <= current || next >= end {
970 return None;
971 }
972 Some(next)
973}
974
975fn select_role_records(
987 role: &Role,
988 records: &[FetchedRecord],
989) -> Result<RoleRecords, CoordinationError> {
990 let mut selected = RoleRecords::new();
991 for record in records {
992 let Some(bytes) = record.key.as_deref() else {
993 continue;
994 };
995 let Ok(key) = decode_key(bytes) else {
996 continue;
997 };
998 if key.role != *role {
999 continue;
1000 }
1001 let value = decode_value(key.kind, record.value.as_deref())?;
1002 selected.push((record.offset, key, value));
1003 }
1004 Ok(selected)
1005}
1006
1007fn registration_record(
1009 role: &Role,
1010 member: &MemberId,
1011 partition: i32,
1012 registered_at: i64,
1013) -> ProducerRecord {
1014 coordination_record(
1015 &CoordinationKey::registration(role.clone(), member.clone()),
1016 &CoordinationRecord::Registration(Registration {
1017 member: member.clone(),
1018 registered_at,
1019 }),
1020 partition,
1021 )
1022}
1023
1024fn lease_record(role: &Role, lease: &Lease, partition: i32) -> ProducerRecord {
1026 coordination_record(
1027 &CoordinationKey::lease(role.clone()),
1028 &CoordinationRecord::Lease(lease.clone()),
1029 partition,
1030 )
1031}
1032
1033fn coordination_record(
1035 key: &CoordinationKey,
1036 value: &CoordinationRecord,
1037 partition: i32,
1038) -> ProducerRecord {
1039 ProducerRecord {
1040 topic: COORDINATION_STATE_TOPIC.to_owned(),
1041 partition: Some(partition),
1042 key: Some(encode_key(key)),
1043 value: encode_value(value),
1044 headers: Vec::new(),
1045 timestamp_ms: None,
1046 }
1047}
1048
1049async fn send_one(
1051 producer: &Producer,
1052 record: ProducerRecord,
1053) -> Result<RecordMetadata, ProducerError> {
1054 producer.send(record).await
1055}
1056
1057fn is_fenced(error: &ProducerError) -> bool {
1064 match error {
1065 ProducerError::FencedProducer => true,
1066 ProducerError::Server(code) => {
1067 matches!(*code, INVALID_PRODUCER_EPOCH | PRODUCER_FENCED)
1068 }
1069 ProducerError::Client(ClientError::Server { error_code }) => {
1070 matches!(*error_code, INVALID_PRODUCER_EPOCH | PRODUCER_FENCED)
1071 }
1072 _ => false,
1073 }
1074}
1075
1076fn fenced_or(role: &Role, error: ProducerError) -> CoordinationError {
1081 if is_fenced(&error) {
1082 return CoordinationError::Fenced { role: role.clone() };
1083 }
1084 CoordinationError::Producer(error)
1085}
1086
1087fn token_from_description(description: &TransactionDescription) -> Option<FencingToken> {
1092 FencingToken::new(description.producer_id, description.producer_epoch).ok()
1093}
1094
1095fn reports_unknown_transactional_id(error: &AdminError) -> bool {
1097 matches!(
1098 error,
1099 AdminError::Broker {
1100 code: TRANSACTIONAL_ID_NOT_FOUND,
1101 ..
1102 }
1103 )
1104}
1105
1106fn server(error_code: i16) -> CoordinationError {
1108 CoordinationError::Client(ClientError::Server { error_code })
1109}
1110
1111fn current_millis() -> i64 {
1117 SystemTime::now()
1118 .duration_since(UNIX_EPOCH)
1119 .map_or(0, |since| {
1120 i64::try_from(since.as_millis()).unwrap_or(i64::MAX)
1121 })
1122}
1123
1124async fn close_producer(role: &Role, producer: Arc<Producer>) {
1126 let Some(producer) = Arc::into_inner(producer) else {
1127 tracing::warn!(
1128 role = %role,
1129 "a leadership handle still shares the producer of this role, so it stays open",
1130 );
1131 return;
1132 };
1133 if let Err(error) = producer.close().await {
1134 tracing::warn!(role = %role, %error, "the producer of a role did not close cleanly");
1135 }
1136}
1137
1138#[cfg(test)]
1139mod mock_tests;
1140
1141#[cfg(test)]
1142mod tests {
1143 #[test]
1147 fn the_list_offsets_timestamps_are_the_sentinels_kafka_defines() {
1148 check!(LIST_OFFSETS_EARLIEST == -2);
1149 check!(LIST_OFFSETS_LATEST == -1);
1150 check!(READ_COMMITTED == 1);
1151 }
1152
1153 #[test]
1157 fn the_leader_of_a_partition_comes_from_the_metadata_response() {
1158 use metadata_response::{MetadataResponsePartition, MetadataResponseTopic};
1159
1160 let response = |partition: i32, leader_id: i32, error_code: i16| MetadataResponse {
1161 topics: vec![MetadataResponseTopic {
1162 name: Some(COORDINATION_STATE_TOPIC.to_owned()),
1163 partitions: vec![MetadataResponsePartition {
1164 partition_index: partition,
1165 leader_id,
1166 error_code,
1167 ..Default::default()
1168 }],
1169 ..Default::default()
1170 }],
1171 ..Default::default()
1172 };
1173
1174 check!(partition_leader(&response(3, 0, 0), 3).unwrap() == 0);
1176 check!(partition_leader(&response(3, 7, 0), 3).unwrap() == 7);
1177
1178 let code = |result: Result<i32, CoordinationError>| {
1179 broker_code(&result.expect_err("the lookup fails"))
1180 };
1181
1182 check!(code(partition_leader(&response(3, -1, 0), 3)) == Some(LEADER_NOT_AVAILABLE));
1184 check!(code(partition_leader(&response(3, 7, 9), 3)) == Some(9));
1186 check!(code(partition_leader(&response(3, 7, 0), 4)) == Some(UNKNOWN_TOPIC_OR_PARTITION));
1188 check!(
1190 code(partition_leader(&MetadataResponse::default(), 0))
1191 == Some(UNKNOWN_TOPIC_OR_PARTITION)
1192 );
1193 }
1194
1195 #[tokio::test]
1196 async fn address_resolution_returns_every_address_and_rejects_an_empty_list() {
1197 let timeout = ClientDnsTimeout::default();
1198
1199 let resolved = resolve_addresses(&["127.0.0.1:9092".to_owned()], timeout)
1200 .await
1201 .expect("a literal address resolves");
1202 check!(resolved.len() == 1);
1203 check!(resolved[0].0.port() == 9092);
1204 check!(resolved[0].1 == "127.0.0.1");
1205
1206 let both = resolve_addresses(
1207 &["127.0.0.1:9092".to_owned(), "127.0.0.1:9093".to_owned()],
1208 timeout,
1209 )
1210 .await
1211 .expect("both literal addresses resolve");
1212 check!(both.len() == 2);
1213
1214 let empty = resolve_addresses(&[], timeout).await;
1216 check!(matches!(empty, Err(CoordinationError::InvalidConfig(_))));
1217 }
1218
1219 #[tokio::test]
1220 async fn a_bootstrap_lookup_that_fails_or_times_out_is_a_config_error() {
1221 let failed = resolve_addresses(
1223 &["broker-without-port".to_owned()],
1224 ClientDnsTimeout::default(),
1225 )
1226 .await;
1227 assert!(let Err(CoordinationError::InvalidConfig(message)) = failed);
1228 check!(message == "the DNS lookup of broker-without-port failed: invalid socket address");
1229
1230 let unanswered = timeout(
1232 Duration::ZERO,
1233 future::pending::<io::Result<Vec<SocketAddr>>>(),
1234 )
1235 .await;
1236 assert!(
1237 let Err(CoordinationError::InvalidConfig(message)) =
1238 lookup_addresses("broker:9092", unanswered)
1239 );
1240 check!(message == "the DNS lookup of broker:9092 timed out");
1241 }
1242
1243 use std::{collections::BTreeSet, future, time::Duration};
1244
1245 use assert2::{assert, check};
1246 use krabka_client_admin::KafkaError;
1247 use krabka_client_core::mock_test_support::recorded_levels;
1248 use krabka_protocol::owned::{
1249 list_offsets_response::{ListOffsetsPartitionResponse, ListOffsetsTopicResponse},
1250 metadata_response::{
1251 MetadataResponseBroker, MetadataResponsePartition, MetadataResponseTopic,
1252 },
1253 };
1254 use krabka_units::secs;
1255
1256 use super::*;
1257 use crate::record::RecordKind;
1258
1259 const GRANTED_AT: i64 = 1_700_000_000_000;
1260 const DEADLINE: i64 = 1_700_000_030_000;
1261
1262 fn role(name: &str) -> Role {
1263 Role::new(name).expect("a valid role")
1264 }
1265
1266 fn member(name: &str) -> MemberId {
1267 MemberId::new(name).expect("a valid member id")
1268 }
1269
1270 fn token(producer_id: i64, producer_epoch: i16) -> FencingToken {
1271 FencingToken::new(producer_id, producer_epoch).expect("a valid token")
1272 }
1273
1274 fn lease(member_name: &str) -> Lease {
1275 Lease {
1276 member: member(member_name),
1277 token: token(4242, 7),
1278 granted_at: GRANTED_AT,
1279 deadline: DEADLINE,
1280 }
1281 }
1282
1283 fn fetched(offset: i64, key: Option<&[u8]>, value: Option<&[u8]>) -> FetchedRecord {
1284 FetchedRecord {
1285 offset,
1286 key: key.map(bytes::Bytes::copy_from_slice),
1287 value: value.map(bytes::Bytes::copy_from_slice),
1288 timestamp: GRANTED_AT,
1289 headers: Vec::new(),
1290 }
1291 }
1292
1293 fn registration_bytes(role_name: &str, member_name: &str) -> (Vec<u8>, Vec<u8>) {
1294 let key = CoordinationKey::registration(role(role_name), member(member_name));
1295 let value = CoordinationRecord::Registration(Registration {
1296 member: member(member_name),
1297 registered_at: GRANTED_AT,
1298 });
1299 (
1300 encode_key(&key).to_vec(),
1301 encode_value(&value)
1302 .expect("a registration is not a tombstone")
1303 .to_vec(),
1304 )
1305 }
1306
1307 fn metadata_topic(partitions: Vec<MetadataResponsePartition>) -> MetadataResponseTopic {
1308 MetadataResponseTopic {
1309 name: Some(COORDINATION_STATE_TOPIC.to_owned()),
1310 topic_id: WireUuid([9; 16]),
1311 partitions,
1312 ..Default::default()
1313 }
1314 }
1315
1316 fn metadata_partition(index: i32, leader: i32) -> MetadataResponsePartition {
1317 MetadataResponsePartition {
1318 partition_index: index,
1319 leader_id: leader,
1320 ..Default::default()
1321 }
1322 }
1323
1324 fn metadata(topics: Vec<MetadataResponseTopic>) -> MetadataResponse {
1325 MetadataResponse {
1326 topics,
1327 ..Default::default()
1328 }
1329 }
1330
1331 fn offsets_response(partition: i32, error_code: i16, offset: i64) -> ListOffsetsResponse {
1332 ListOffsetsResponse {
1333 topics: vec![ListOffsetsTopicResponse {
1334 name: COORDINATION_STATE_TOPIC.to_owned(),
1335 partitions: vec![ListOffsetsPartitionResponse {
1336 partition_index: partition,
1337 error_code,
1338 offset,
1339 ..Default::default()
1340 }],
1341 ..Default::default()
1342 }],
1343 ..Default::default()
1344 }
1345 }
1346
1347 fn broker_code(error: &CoordinationError) -> Option<i16> {
1348 match error {
1349 CoordinationError::Client(ClientError::Server { error_code }) => Some(*error_code),
1350 CoordinationError::Admin(AdminError::Broker { code, .. }) => Some(*code),
1351 _ => None,
1352 }
1353 }
1354
1355 #[test]
1360 fn a_role_maps_onto_a_frozen_partition() {
1361 let cases = [
1362 ("controller", 16, 12),
1363 ("dispatcher", 16, 10),
1364 ("role-a", 16, 10),
1365 ("role-b", 16, 12),
1366 ("controller", 1, 0),
1367 ];
1368 for (name, partitions, want) in cases {
1369 check!(
1370 role_partition(&role(name), partitions).unwrap() == want,
1371 "role {name} over {partitions} partitions"
1372 );
1373 }
1374 }
1375
1376 #[test]
1380 fn both_record_kinds_of_a_role_take_the_same_partition() {
1381 let name = role("controller");
1382 let partition = role_partition(&name, 16).unwrap();
1383 let registration = registration_record(&name, &member("node-1"), partition, GRANTED_AT);
1384 let lease = lease_record(&name, &lease("node-1"), partition);
1385 check!(registration.partition == lease.partition);
1386 check!(registration.partition == Some(partition));
1387 check!(registration.key != lease.key);
1388 }
1389
1390 #[test]
1391 fn a_role_partition_stays_inside_the_partition_count() {
1392 for count in [1_i32, 2, 3, 7, 16, 50] {
1393 for name in ["a", "controller", "a-very-long-role-name-0123456789"] {
1394 let partition = role_partition(&role(name), count).unwrap();
1395 check!(partition >= 0, "role {name} over {count} partitions");
1396 check!(partition < count, "role {name} over {count} partitions");
1397 }
1398 }
1399 }
1400
1401 #[test]
1402 fn a_role_partition_rejects_a_topic_with_no_partition() {
1403 for count in [0_i32, -1, i32::MIN] {
1404 assert!(let Err(CoordinationError::InvalidConfig(_)) =
1405 role_partition(&role("controller"), count));
1406 }
1407 }
1408
1409 #[test]
1410 fn a_bootstrap_list_splits_on_commas_and_drops_the_empty_entries() {
1411 let cases: [(&str, Vec<String>); 5] = [
1412 ("localhost:9092", vec!["localhost:9092".to_owned()]),
1413 (
1414 "a:1, b:2 ,c:3",
1415 vec!["a:1".to_owned(), "b:2".to_owned(), "c:3".to_owned()],
1416 ),
1417 (",a:1,,", vec!["a:1".to_owned()]),
1418 ("", Vec::new()),
1419 (" , ", Vec::new()),
1420 ];
1421 for (input, want) in cases {
1422 check!(bootstrap_addresses(input) == want, "bootstrap {input:?}");
1423 }
1424 }
1425
1426 #[test]
1427 fn a_topic_shape_needs_a_partition_and_a_replica() {
1428 check!(check_topic_shape(16, 3).is_ok());
1429 check!(check_topic_shape(1, 1).is_ok());
1430 assert!(let Err(CoordinationError::InvalidConfig(_)) = check_topic_shape(0, 3));
1431 assert!(let Err(CoordinationError::InvalidConfig(_)) = check_topic_shape(16, 0));
1432 assert!(let Err(CoordinationError::InvalidConfig(_)) = check_topic_shape(-1, -1));
1433 }
1434
1435 #[test]
1438 fn the_topic_spec_asks_for_a_compacted_topic() {
1439 let spec = coordination_topic_spec(16, 3);
1440 check!(spec.name == COORDINATION_STATE_TOPIC);
1441 check!(spec.partitions == 16);
1442 check!(spec.replicas == 3);
1443 check!(
1444 spec.configs == maplit::btreemap! {"cleanup.policy".to_owned() => "compact".to_owned()}
1445 );
1446 }
1447
1448 #[test]
1451 fn topic_creation_takes_an_existing_topic_for_success() {
1452 let outcome = |code: Option<i16>| CreateTopicOutcome {
1453 name: COORDINATION_STATE_TOPIC.to_owned(),
1454 topic_id: None,
1455 error: code.map(|code| KafkaError {
1456 code,
1457 name: "TEST",
1458 message: None,
1459 }),
1460 throttle_time: None,
1461 };
1462 check!(accept_topic_creation(&[]).is_ok());
1463 check!(accept_topic_creation(&[outcome(None)]).is_ok());
1464 check!(accept_topic_creation(&[outcome(Some(TOPIC_ALREADY_EXISTS))]).is_ok());
1465
1466 let refused = accept_topic_creation(&[outcome(Some(38))])
1467 .expect_err("an invalid replication factor is a fault");
1468 check!(broker_code(&refused) == Some(38));
1469 }
1470
1471 #[test]
1472 fn the_metadata_request_names_the_topic_and_never_creates_it() {
1473 check!(
1474 coordination_metadata_request()
1475 == MetadataRequest {
1476 topics: Some(vec![MetadataRequestTopic {
1477 name: Some(COORDINATION_STATE_TOPIC.to_owned()),
1478 ..Default::default()
1479 }]),
1480 allow_auto_topic_creation: false,
1481 ..Default::default()
1482 }
1483 );
1484 }
1485
1486 #[test]
1487 fn the_topic_layout_reads_the_partition_count_and_the_topic_id() {
1488 let response = metadata(vec![metadata_topic(vec![
1489 metadata_partition(0, 1),
1490 metadata_partition(1, 2),
1491 ])]);
1492 check!(
1493 topic_layout(&response).unwrap()
1494 == TopicLayout {
1495 partitions: 2,
1496 topic_id: WireUuid([9; 16]),
1497 }
1498 );
1499 }
1500
1501 #[test]
1502 fn the_topic_layout_reports_a_topic_the_cluster_does_not_have() {
1503 let absent = metadata(Vec::new());
1504 check!(
1505 broker_code(&topic_layout(&absent).unwrap_err()) == Some(UNKNOWN_TOPIC_OR_PARTITION)
1506 );
1507
1508 let empty = metadata(vec![metadata_topic(Vec::new())]);
1509 check!(broker_code(&topic_layout(&empty).unwrap_err()) == Some(UNKNOWN_TOPIC_OR_PARTITION));
1510
1511 let refused = metadata(vec![MetadataResponseTopic {
1512 error_code: 29,
1513 ..metadata_topic(vec![metadata_partition(0, 1)])
1514 }]);
1515 check!(broker_code(&topic_layout(&refused).unwrap_err()) == Some(29));
1516 }
1517
1518 #[test]
1519 fn the_partition_leader_comes_from_the_metadata_row_of_that_partition() {
1520 let response = metadata(vec![metadata_topic(vec![
1521 metadata_partition(0, 11),
1522 metadata_partition(1, 22),
1523 ])]);
1524 check!(partition_leader(&response, 0).unwrap() == 11);
1525 check!(partition_leader(&response, 1).unwrap() == 22);
1526 check!(
1527 broker_code(&partition_leader(&response, 9).unwrap_err())
1528 == Some(UNKNOWN_TOPIC_OR_PARTITION)
1529 );
1530 }
1531
1532 #[test]
1533 fn a_partition_with_no_leader_is_a_broker_error() {
1534 let without_leader = metadata(vec![metadata_topic(vec![metadata_partition(0, -1)])]);
1535 check!(
1536 broker_code(&partition_leader(&without_leader, 0).unwrap_err())
1537 == Some(LEADER_NOT_AVAILABLE)
1538 );
1539
1540 let refused = metadata(vec![metadata_topic(vec![MetadataResponsePartition {
1541 error_code: 9,
1542 ..metadata_partition(0, 1)
1543 }])]);
1544 check!(broker_code(&partition_leader(&refused, 0).unwrap_err()) == Some(9));
1545 }
1546
1547 #[test]
1548 fn the_broker_list_carries_every_field_the_pool_needs() {
1549 let response = MetadataResponse {
1550 brokers: vec![MetadataResponseBroker {
1551 node_id: 3,
1552 host: "broker-3".to_owned(),
1553 port: 9092,
1554 rack: Some("rack-a".to_owned()),
1555 ..Default::default()
1556 }],
1557 ..Default::default()
1558 };
1559 let brokers = brokers_of(&response);
1560 check!(brokers.len() == 1);
1561 check!(brokers[0].id == 3);
1562 check!(brokers[0].host == "broker-3");
1563 check!(brokers[0].port == 9092);
1564 check!(brokers[0].rack.as_deref() == Some("rack-a"));
1565 }
1566
1567 #[test]
1568 fn the_offset_request_asks_one_partition_at_committed_isolation() {
1569 check!(
1570 list_offsets_request(4, LIST_OFFSETS_LATEST)
1571 == ListOffsetsRequest {
1572 replica_id: -1,
1573 isolation_level: READ_COMMITTED,
1574 topics: vec![ListOffsetsTopic {
1575 name: COORDINATION_STATE_TOPIC.to_owned(),
1576 partitions: vec![ListOffsetsPartition {
1577 partition_index: 4,
1578 current_leader_epoch: -1,
1579 timestamp: LIST_OFFSETS_LATEST,
1580 ..Default::default()
1581 }],
1582 ..Default::default()
1583 }],
1584 ..Default::default()
1585 }
1586 );
1587 }
1588
1589 #[test]
1590 fn the_offset_response_gives_the_offset_of_the_asked_partition() {
1591 check!(list_offsets_offset(&offsets_response(4, 0, 91), 4).unwrap() == 91);
1592 check!(
1593 broker_code(&list_offsets_offset(&offsets_response(4, 0, 91), 5).unwrap_err())
1594 == Some(UNKNOWN_TOPIC_OR_PARTITION)
1595 );
1596 check!(
1597 broker_code(&list_offsets_offset(&offsets_response(4, 6, -1), 4).unwrap_err())
1598 == Some(6)
1599 );
1600 }
1601
1602 #[test]
1606 fn the_scan_advances_until_it_reaches_the_end_it_started_with() {
1607 let cases = [
1608 ("a batch inside the range", 0, 10, Some(4), Some(4)),
1609 ("a batch that reaches the end", 0, 10, Some(10), None),
1610 ("a batch past the end", 0, 10, Some(11), None),
1611 ("no batch in the response", 0, 10, None, None),
1612 ("a cursor that did not move", 4, 10, Some(4), None),
1613 ("a cursor that went backwards", 4, 10, Some(2), None),
1614 ];
1615 for (name, current, end, next_offset, want) in cases {
1616 check!(
1617 next_scan_offset(current, end, next_offset) == want,
1618 "{name}"
1619 );
1620 }
1621 }
1622
1623 #[test]
1624 fn the_scan_keeps_the_records_of_the_role_in_offset_order() {
1625 let (mine_key, mine_value) = registration_bytes("controller", "node-1");
1626 let (other_key, other_value) = registration_bytes("dispatcher", "node-9");
1627 let records = [
1628 fetched(4, Some(&other_key), Some(&other_value)),
1629 fetched(5, Some(&mine_key), Some(&mine_value)),
1630 fetched(6, Some(b"not a coordination key"), Some(&mine_value)),
1631 fetched(7, None, Some(&mine_value)),
1632 fetched(8, Some(&mine_key), None),
1633 ];
1634
1635 let selected = select_role_records(&role("controller"), &records).unwrap();
1636 check!(
1637 selected
1638 == vec![
1639 (
1640 5,
1641 CoordinationKey::registration(role("controller"), member("node-1")),
1642 CoordinationRecord::Registration(Registration {
1643 member: member("node-1"),
1644 registered_at: GRANTED_AT,
1645 }),
1646 ),
1647 (
1648 8,
1649 CoordinationKey::registration(role("controller"), member("node-1")),
1650 CoordinationRecord::Tombstone,
1651 ),
1652 ]
1653 );
1654 }
1655
1656 #[test]
1657 fn a_malformed_value_under_a_key_of_the_role_is_a_fault() {
1658 let (key, _) = registration_bytes("controller", "node-1");
1659 let records = [fetched(1, Some(&key), Some(b"truncated"))];
1660 assert!(let Err(CoordinationError::Record(_)) =
1661 select_role_records(&role("controller"), &records));
1662 }
1663
1664 #[test]
1665 fn a_registration_record_carries_the_frozen_key_and_value() {
1666 let name = role("controller");
1667 let who = member("node-1");
1668 let key = CoordinationKey::registration(name.clone(), who.clone());
1669 let value = CoordinationRecord::Registration(Registration {
1670 member: who.clone(),
1671 registered_at: GRANTED_AT,
1672 });
1673 check!(
1674 registration_record(&name, &who, 3, GRANTED_AT)
1675 == ProducerRecord {
1676 topic: COORDINATION_STATE_TOPIC.to_owned(),
1677 partition: Some(3),
1678 key: Some(encode_key(&key)),
1679 value: encode_value(&value),
1680 headers: Vec::new(),
1681 timestamp_ms: None,
1682 }
1683 );
1684 }
1685
1686 #[test]
1687 fn a_lease_record_carries_the_frozen_key_and_value() {
1688 let name = role("controller");
1689 let held = lease("node-1");
1690 let key = CoordinationKey::lease(name.clone());
1691 let value = CoordinationRecord::Lease(held.clone());
1692 check!(
1693 lease_record(&name, &held, 3)
1694 == ProducerRecord {
1695 topic: COORDINATION_STATE_TOPIC.to_owned(),
1696 partition: Some(3),
1697 key: Some(encode_key(&key)),
1698 value: encode_value(&value),
1699 headers: Vec::new(),
1700 timestamp_ms: None,
1701 }
1702 );
1703 let decoded = decode_key(&encode_key(&key)).unwrap();
1704 check!(decoded.kind == RecordKind::Lease);
1705 check!(decoded.member.is_none());
1706 }
1707
1708 #[test]
1712 fn a_fence_from_the_producer_becomes_the_loss_of_the_role() {
1713 let fences = [
1714 ProducerError::FencedProducer,
1715 ProducerError::Server(INVALID_PRODUCER_EPOCH),
1716 ProducerError::Server(PRODUCER_FENCED),
1717 ProducerError::Client(ClientError::Server {
1718 error_code: INVALID_PRODUCER_EPOCH,
1719 }),
1720 ProducerError::Client(ClientError::Server {
1721 error_code: PRODUCER_FENCED,
1722 }),
1723 ];
1724 for error in fences {
1725 let reported = error.to_string();
1726 check!(is_fenced(&error), "producer error {reported}");
1727 let mapped = fenced_or(&role("controller"), error);
1728 check!(mapped.is_fenced(), "producer error {reported}");
1729 assert!(let CoordinationError::Fenced { .. } = &mapped);
1730 check!(mapped.to_string() == "fenced: another member holds role controller");
1731 }
1732 }
1733
1734 #[test]
1735 fn a_fault_that_is_not_a_fence_passes_through() {
1736 let faults = [
1737 ProducerError::Closed,
1738 ProducerError::FlushTimeout,
1739 ProducerError::Server(1),
1740 ProducerError::Client(ClientError::Disconnected),
1741 ProducerError::Client(ClientError::Server { error_code: 46 }),
1742 ];
1743 for error in faults {
1744 let reported = error.to_string();
1745 check!(!is_fenced(&error), "producer error {reported}");
1746 let mapped = fenced_or(&role("controller"), error);
1747 check!(!mapped.is_fenced(), "producer error {reported}");
1748 assert!(let CoordinationError::Producer(_) = &mapped);
1749 }
1750 }
1751
1752 #[test]
1753 fn a_described_transaction_becomes_a_fencing_token() {
1754 let describe = |producer_id, producer_epoch| TransactionDescription {
1755 transactional_id: "controller".to_owned(),
1756 coordinator_id: 1,
1757 state: "Empty".to_owned(),
1758 producer_id,
1759 producer_epoch,
1760 timeout: secs(30),
1761 start_time_ms: None,
1762 topic_partitions: BTreeSet::new(),
1763 };
1764 check!(token_from_description(&describe(4242, 7)) == Some(token(4242, 7)));
1765 check!(token_from_description(&describe(0, 0)) == Some(token(0, 0)));
1766 check!(token_from_description(&describe(-1, -1)).is_none());
1767 check!(token_from_description(&describe(4242, -1)).is_none());
1768 }
1769
1770 #[test]
1773 fn an_unknown_transactional_id_is_not_a_fault() {
1774 let broker = |code| AdminError::Broker {
1775 api: "DescribeTransactions",
1776 code,
1777 name: "TEST",
1778 message: None,
1779 };
1780 check!(reports_unknown_transactional_id(&broker(
1781 TRANSACTIONAL_ID_NOT_FOUND
1782 )));
1783 check!(!reports_unknown_transactional_id(&broker(16)));
1784 check!(!reports_unknown_transactional_id(&AdminError::Protocol(
1785 "no row".to_owned()
1786 )));
1787 }
1788
1789 #[test]
1790 fn the_clock_reports_an_instant_after_2020() {
1791 check!(current_millis() > 1_577_836_800_000);
1792 }
1793
1794 #[tokio::test]
1797 async fn the_builder_refuses_a_configuration_kafka_would_reject() {
1798 assert!(matches!(
1799 BrokerTransport::builder().bootstrap("").build().await,
1800 Err(CoordinationError::InvalidConfig(_))
1801 ));
1802 assert!(matches!(
1803 BrokerTransport::builder()
1804 .bootstrap("localhost:9092")
1805 .topic_partitions(0)
1806 .build()
1807 .await,
1808 Err(CoordinationError::InvalidConfig(_))
1809 ));
1810 assert!(matches!(
1811 BrokerTransport::builder()
1812 .bootstrap("localhost:9092")
1813 .topic_replication(0)
1814 .build()
1815 .await,
1816 Err(CoordinationError::InvalidConfig(_))
1817 ));
1818 }
1819
1820 #[tokio::test]
1824 async fn a_refused_start_is_recorded_at_its_span_level() {
1825 let (result, levels) =
1826 recorded_levels(BrokerTransport::builder().bootstrap("").build()).await;
1827 assert!(let Err(CoordinationError::InvalidConfig(_)) = result);
1828 check!(levels == vec![tracing::Level::INFO]);
1829 }
1830}