1use core::time::Duration;
45use std::collections::{HashMap, HashSet};
46use std::task::Waker;
47
48use slab::Slab;
49
50use crate::pb;
51use crate::types::RequestId;
52
53#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
58pub struct TxnId {
59 pub most_sig_bits: u64,
61 pub least_sig_bits: u64,
63}
64
65impl TxnId {
66 pub const fn new(most_sig_bits: u64, least_sig_bits: u64) -> Self {
68 Self {
69 most_sig_bits,
70 least_sig_bits,
71 }
72 }
73}
74
75impl core::fmt::Display for TxnId {
76 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
77 write!(f, "{}:{}", self.most_sig_bits, self.least_sig_bits)
78 }
79}
80
81#[derive(Debug, Clone, Copy, PartialEq, Eq)]
85pub enum TxnState {
86 Open,
88 Committing,
90 Committed,
92 Aborting,
94 Aborted,
96 Errored,
98}
99
100#[derive(Debug, Clone, Copy, PartialEq, Eq)]
102pub enum TxnAction {
103 Commit,
106 Abort,
108}
109
110impl TxnAction {
111 pub const fn to_pb(self) -> pb::TxnAction {
113 match self {
114 Self::Commit => pb::TxnAction::Commit,
115 Self::Abort => pb::TxnAction::Abort,
116 }
117 }
118}
119
120#[derive(Debug, Clone, thiserror::Error)]
126pub enum TxnError {
127 #[error("transaction conflict")]
130 Conflict,
131 #[error("transaction not found")]
134 NotFound,
135 #[error("transaction timed out")]
138 Timeout,
139 #[error("transaction aborted")]
141 Aborted,
142 #[error("broker error {0}: {1}")]
144 Broker(i32, String),
145}
146
147impl TxnError {
148 pub fn from_broker(code: i32, message: String) -> Self {
150 match pb::ServerError::try_from(code) {
153 Ok(pb::ServerError::TransactionConflict) => Self::Conflict,
154 Ok(
155 pb::ServerError::TransactionNotFound
156 | pb::ServerError::TransactionCoordinatorNotFound,
157 ) => Self::NotFound,
158 Ok(pb::ServerError::InvalidTxnStatus) => Self::Aborted,
159 _ => Self::Broker(code, message),
160 }
161 }
162}
163
164#[derive(Debug, Clone)]
170pub struct TransactionMetadata {
171 pub id: TxnId,
173 pub state: TxnState,
175 pub coordinator_id: u64,
177 pub timeout: Duration,
179 pub produced_topics: HashSet<String>,
181 pub acked_subscriptions: HashMap<String, Vec<String>>,
183}
184
185impl TransactionMetadata {
186 fn new(id: TxnId, coordinator_id: u64, timeout: Duration) -> Self {
187 Self {
188 id,
189 state: TxnState::Open,
190 coordinator_id,
191 timeout,
192 produced_topics: HashSet::new(),
193 acked_subscriptions: HashMap::new(),
194 }
195 }
196}
197
198#[allow(dead_code)]
204#[derive(Debug, Clone)]
205struct PendingNewTxn {
206 request_id: RequestId,
207 waker_key: usize,
208}
209
210#[allow(dead_code)]
213#[derive(Debug, Clone)]
214struct PendingAddPartition {
215 request_id: RequestId,
216 txn: TxnId,
217 topic: String,
218 waker_key: usize,
219}
220
221#[allow(dead_code)]
223#[derive(Debug, Clone)]
224struct PendingAddSubscription {
225 request_id: RequestId,
226 txn: TxnId,
227 subscription: String,
228 topic: String,
229 waker_key: usize,
230}
231
232#[allow(dead_code)]
234#[derive(Debug, Clone)]
235struct PendingEndTxn {
236 request_id: RequestId,
237 txn: TxnId,
238 action: TxnAction,
239 waker_key: usize,
240}
241
242#[derive(Debug)]
248pub struct TxnClient {
249 coordinator_id: u64,
250 pending_new_txn: Slab<Waker>,
252 new_txn_by_request: HashMap<RequestId, PendingNewTxn>,
253 pending_add_partition: Slab<Waker>,
255 add_partition_by_request: HashMap<RequestId, PendingAddPartition>,
256 pending_add_subscription: Slab<Waker>,
258 add_subscription_by_request: HashMap<RequestId, PendingAddSubscription>,
259 pending_end_txn: Slab<Waker>,
261 end_txn_by_request: HashMap<RequestId, PendingEndTxn>,
262 transactions: HashMap<TxnId, TransactionMetadata>,
264}
265
266impl TxnClient {
267 pub fn new(coordinator_id: u64) -> Self {
269 Self {
270 coordinator_id,
271 pending_new_txn: Slab::new(),
272 new_txn_by_request: HashMap::new(),
273 pending_add_partition: Slab::new(),
274 add_partition_by_request: HashMap::new(),
275 pending_add_subscription: Slab::new(),
276 add_subscription_by_request: HashMap::new(),
277 pending_end_txn: Slab::new(),
278 end_txn_by_request: HashMap::new(),
279 transactions: HashMap::new(),
280 }
281 }
282
283 pub const fn coordinator_id(&self) -> u64 {
285 self.coordinator_id
286 }
287
288 pub fn transaction(&self, id: TxnId) -> Option<&TransactionMetadata> {
290 self.transactions.get(&id)
291 }
292
293 pub fn len(&self) -> usize {
295 self.transactions.len()
296 }
297
298 pub fn is_empty(&self) -> bool {
300 self.transactions.is_empty()
301 }
302
303 pub fn register_new_txn_waker(&mut self, request_id: RequestId, waker: Waker) {
307 if let Some(pending) = self.new_txn_by_request.get_mut(&request_id) {
308 self.pending_new_txn[pending.waker_key] = waker;
309 }
310 }
311
312 pub fn register_add_partition_waker(&mut self, request_id: RequestId, waker: Waker) {
314 if let Some(pending) = self.add_partition_by_request.get_mut(&request_id) {
315 self.pending_add_partition[pending.waker_key] = waker;
316 }
317 }
318
319 pub fn register_add_subscription_waker(&mut self, request_id: RequestId, waker: Waker) {
321 if let Some(pending) = self.add_subscription_by_request.get_mut(&request_id) {
322 self.pending_add_subscription[pending.waker_key] = waker;
323 }
324 }
325
326 pub fn register_end_txn_waker(&mut self, request_id: RequestId, waker: Waker) {
328 if let Some(pending) = self.end_txn_by_request.get_mut(&request_id) {
329 self.pending_end_txn[pending.waker_key] = waker;
330 }
331 }
332
333 pub fn new_txn(&mut self, request_id: u64, timeout_ms: u64) -> pb::CommandNewTxn {
348 let waker_key = self.pending_new_txn.insert(noop_waker());
349 let pending = PendingNewTxn {
350 request_id: RequestId(request_id),
351 waker_key,
352 };
353 self.new_txn_by_request
354 .insert(RequestId(request_id), pending);
355 pb::CommandNewTxn {
356 request_id,
357 txn_ttl_millis: Some(timeout_ms),
358 tc_id: Some(self.coordinator_id),
359 scalable: None,
362 }
363 }
364
365 pub fn handle_new_txn_response(
371 &mut self,
372 resp: pb::CommandNewTxnResponse,
373 ) -> Result<Option<TxnId>, TxnError> {
374 let request_id = RequestId(resp.request_id);
375 let Some(pending) = self.new_txn_by_request.remove(&request_id) else {
376 return Ok(None);
377 };
378 let waker = self.pending_new_txn.try_remove(pending.waker_key);
379
380 if let Some(code) = resp.error {
381 if let Some(w) = waker {
382 w.wake();
383 }
384 return Err(TxnError::from_broker(
385 code,
386 resp.message.unwrap_or_default(),
387 ));
388 }
389
390 let txn_id = TxnId::new(
391 resp.txnid_most_bits.unwrap_or(0),
392 resp.txnid_least_bits.unwrap_or(0),
393 );
394 let timeout = Duration::from_secs(0); let metadata = TransactionMetadata::new(txn_id, self.coordinator_id, timeout);
396 self.transactions.insert(txn_id, metadata);
397 if let Some(w) = waker {
398 w.wake();
399 }
400 Ok(Some(txn_id))
401 }
402
403 pub fn add_partition(
406 &mut self,
407 request_id: u64,
408 txn: TxnId,
409 topic: String,
410 ) -> pb::CommandAddPartitionToTxn {
411 let waker_key = self.pending_add_partition.insert(noop_waker());
412 let pending = PendingAddPartition {
413 request_id: RequestId(request_id),
414 txn,
415 topic: topic.clone(),
416 waker_key,
417 };
418 self.add_partition_by_request
419 .insert(RequestId(request_id), pending);
420 pb::CommandAddPartitionToTxn {
421 request_id,
422 txnid_least_bits: Some(txn.least_sig_bits),
423 txnid_most_bits: Some(txn.most_sig_bits),
424 partitions: vec![topic],
425 scalable: None,
427 }
428 }
429
430 pub fn handle_add_partition_response(
434 &mut self,
435 resp: pb::CommandAddPartitionToTxnResponse,
436 ) -> Result<(), TxnError> {
437 let request_id = RequestId(resp.request_id);
438 let Some(pending) = self.add_partition_by_request.remove(&request_id) else {
439 return Ok(());
440 };
441 let waker = self.pending_add_partition.try_remove(pending.waker_key);
442
443 if let Some(code) = resp.error {
444 if let Some(meta) = self.transactions.get_mut(&pending.txn) {
445 meta.state = TxnState::Errored;
446 }
447 if let Some(w) = waker {
448 w.wake();
449 }
450 return Err(TxnError::from_broker(
451 code,
452 resp.message.unwrap_or_default(),
453 ));
454 }
455
456 if let Some(meta) = self.transactions.get_mut(&pending.txn) {
457 meta.produced_topics.insert(pending.topic);
458 }
459 if let Some(w) = waker {
460 w.wake();
461 }
462 Ok(())
463 }
464
465 pub fn add_subscription(
468 &mut self,
469 request_id: u64,
470 txn: TxnId,
471 subscription: String,
472 topic: String,
473 ) -> pb::CommandAddSubscriptionToTxn {
474 let waker_key = self.pending_add_subscription.insert(noop_waker());
475 let pending = PendingAddSubscription {
476 request_id: RequestId(request_id),
477 txn,
478 subscription: subscription.clone(),
479 topic: topic.clone(),
480 waker_key,
481 };
482 self.add_subscription_by_request
483 .insert(RequestId(request_id), pending);
484 pb::CommandAddSubscriptionToTxn {
485 request_id,
486 txnid_least_bits: Some(txn.least_sig_bits),
487 txnid_most_bits: Some(txn.most_sig_bits),
488 subscription: vec![pb::Subscription {
489 topic,
490 subscription,
491 }],
492 scalable: None,
494 }
495 }
496
497 pub fn handle_add_subscription_response(
500 &mut self,
501 resp: pb::CommandAddSubscriptionToTxnResponse,
502 ) -> Result<(), TxnError> {
503 let request_id = RequestId(resp.request_id);
504 let Some(pending) = self.add_subscription_by_request.remove(&request_id) else {
505 return Ok(());
506 };
507 let waker = self.pending_add_subscription.try_remove(pending.waker_key);
508
509 if let Some(code) = resp.error {
510 if let Some(meta) = self.transactions.get_mut(&pending.txn) {
511 meta.state = TxnState::Errored;
512 }
513 if let Some(w) = waker {
514 w.wake();
515 }
516 return Err(TxnError::from_broker(
517 code,
518 resp.message.unwrap_or_default(),
519 ));
520 }
521
522 if let Some(meta) = self.transactions.get_mut(&pending.txn) {
523 meta.acked_subscriptions
524 .entry(pending.subscription)
525 .or_default()
526 .push(pending.topic);
527 }
528 if let Some(w) = waker {
529 w.wake();
530 }
531 Ok(())
532 }
533
534 pub fn end_txn(&mut self, request_id: u64, txn: TxnId, action: TxnAction) -> pb::CommandEndTxn {
539 let waker_key = self.pending_end_txn.insert(noop_waker());
540 let pending = PendingEndTxn {
541 request_id: RequestId(request_id),
542 txn,
543 action,
544 waker_key,
545 };
546 self.end_txn_by_request
547 .insert(RequestId(request_id), pending);
548 if let Some(meta) = self.transactions.get_mut(&txn) {
549 meta.state = match action {
550 TxnAction::Commit => TxnState::Committing,
551 TxnAction::Abort => TxnState::Aborting,
552 };
553 }
554 pb::CommandEndTxn {
555 request_id,
556 txnid_least_bits: Some(txn.least_sig_bits),
557 txnid_most_bits: Some(txn.most_sig_bits),
558 txn_action: Some(action.to_pb() as i32),
559 scalable: None,
561 }
562 }
563
564 pub fn handle_end_txn_response(
570 &mut self,
571 resp: pb::CommandEndTxnResponse,
572 ) -> Result<TxnState, TxnError> {
573 let request_id = RequestId(resp.request_id);
574 let Some(pending) = self.end_txn_by_request.remove(&request_id) else {
575 return Ok(TxnState::Errored);
578 };
579 let waker = self.pending_end_txn.try_remove(pending.waker_key);
580
581 if let Some(code) = resp.error {
582 if let Some(meta) = self.transactions.get_mut(&pending.txn) {
583 meta.state = TxnState::Errored;
584 }
585 if let Some(w) = waker {
586 w.wake();
587 }
588 return Err(TxnError::from_broker(
589 code,
590 resp.message.unwrap_or_default(),
591 ));
592 }
593
594 let final_state = match pending.action {
595 TxnAction::Commit => TxnState::Committed,
596 TxnAction::Abort => TxnState::Aborted,
597 };
598 if let Some(meta) = self.transactions.get_mut(&pending.txn) {
599 meta.state = final_state;
600 }
601 if let Some(w) = waker {
602 w.wake();
603 }
604 Ok(final_state)
605 }
606
607 pub fn forget(&mut self, txn: TxnId) {
610 self.transactions.remove(&txn);
611 }
612}
613
614fn noop_waker() -> Waker {
621 Waker::noop().clone()
622}
623
624#[cfg(test)]
625mod tests {
626 use super::*;
627
628 fn ok_new_txn_response(request_id: u64, most: u64, least: u64) -> pb::CommandNewTxnResponse {
629 pb::CommandNewTxnResponse {
630 request_id,
631 txnid_most_bits: Some(most),
632 txnid_least_bits: Some(least),
633 error: None,
634 message: None,
635 }
636 }
637
638 #[test]
639 fn new_txn_round_trip_returns_id_and_marks_open() {
640 let mut client = TxnClient::new(7);
641 let cmd = client.new_txn(1, 30_000);
642 assert_eq!(cmd.request_id, 1);
643 assert_eq!(cmd.tc_id, Some(7));
644 assert_eq!(cmd.txn_ttl_millis, Some(30_000));
649
650 let id = client
651 .handle_new_txn_response(ok_new_txn_response(1, 99, 42))
652 .expect("ok")
653 .expect("txn id present");
654 assert_eq!(id, TxnId::new(99, 42));
655
656 let meta = client.transaction(id).expect("registered");
657 assert_eq!(meta.state, TxnState::Open);
658 assert_eq!(meta.coordinator_id, 7);
659 assert!(meta.produced_topics.is_empty());
660 assert!(meta.acked_subscriptions.is_empty());
661 }
662
663 #[test]
664 fn add_partition_records_topic_on_success() {
665 let mut client = TxnClient::new(0);
666 let _ = client.new_txn(1, 0);
667 let id = client
668 .handle_new_txn_response(ok_new_txn_response(1, 0, 1))
669 .unwrap()
670 .unwrap();
671
672 let cmd = client.add_partition(2, id, "persistent://p/n/t".to_owned());
673 assert_eq!(cmd.request_id, 2);
674 assert_eq!(cmd.txnid_least_bits, Some(1));
675 assert_eq!(cmd.partitions, vec!["persistent://p/n/t".to_owned()]);
676
677 client
678 .handle_add_partition_response(pb::CommandAddPartitionToTxnResponse {
679 request_id: 2,
680 txnid_least_bits: Some(1),
681 txnid_most_bits: Some(0),
682 error: None,
683 message: None,
684 })
685 .expect("ok");
686
687 let meta = client.transaction(id).unwrap();
688 assert!(meta.produced_topics.contains("persistent://p/n/t"));
689 assert_eq!(meta.state, TxnState::Open);
690 }
691
692 #[test]
693 fn add_subscription_records_subscription_on_success() {
694 let mut client = TxnClient::new(0);
695 let _ = client.new_txn(1, 0);
696 let id = client
697 .handle_new_txn_response(ok_new_txn_response(1, 0, 2))
698 .unwrap()
699 .unwrap();
700
701 let cmd =
702 client.add_subscription(3, id, "sub-a".to_owned(), "persistent://p/n/t".to_owned());
703 assert_eq!(cmd.request_id, 3);
704 assert_eq!(cmd.subscription.len(), 1);
705 assert_eq!(cmd.subscription[0].subscription, "sub-a");
706
707 client
708 .handle_add_subscription_response(pb::CommandAddSubscriptionToTxnResponse {
709 request_id: 3,
710 txnid_least_bits: Some(2),
711 txnid_most_bits: Some(0),
712 error: None,
713 message: None,
714 })
715 .expect("ok");
716
717 let meta = client.transaction(id).unwrap();
718 let topics = meta.acked_subscriptions.get("sub-a").expect("present");
719 assert_eq!(topics, &vec!["persistent://p/n/t".to_owned()]);
720 }
721
722 #[test]
723 fn end_txn_commit_happy_path_marks_committed() {
724 let mut client = TxnClient::new(0);
725 let _ = client.new_txn(1, 0);
726 let id = client
727 .handle_new_txn_response(ok_new_txn_response(1, 0, 10))
728 .unwrap()
729 .unwrap();
730
731 let cmd = client.end_txn(2, id, TxnAction::Commit);
732 assert_eq!(cmd.txn_action, Some(pb::TxnAction::Commit as i32));
733 assert_eq!(client.transaction(id).unwrap().state, TxnState::Committing);
734
735 let final_state = client
736 .handle_end_txn_response(pb::CommandEndTxnResponse {
737 request_id: 2,
738 txnid_least_bits: Some(10),
739 txnid_most_bits: Some(0),
740 error: None,
741 message: None,
742 })
743 .expect("ok");
744 assert_eq!(final_state, TxnState::Committed);
745 assert_eq!(client.transaction(id).unwrap().state, TxnState::Committed);
746 }
747
748 #[test]
749 fn end_txn_abort_happy_path_marks_aborted() {
750 let mut client = TxnClient::new(0);
751 let _ = client.new_txn(1, 0);
752 let id = client
753 .handle_new_txn_response(ok_new_txn_response(1, 0, 11))
754 .unwrap()
755 .unwrap();
756
757 let cmd = client.end_txn(2, id, TxnAction::Abort);
758 assert_eq!(cmd.txn_action, Some(pb::TxnAction::Abort as i32));
759 assert_eq!(client.transaction(id).unwrap().state, TxnState::Aborting);
760
761 let final_state = client
762 .handle_end_txn_response(pb::CommandEndTxnResponse {
763 request_id: 2,
764 txnid_least_bits: Some(11),
765 txnid_most_bits: Some(0),
766 error: None,
767 message: None,
768 })
769 .expect("ok");
770 assert_eq!(final_state, TxnState::Aborted);
771 assert_eq!(client.transaction(id).unwrap().state, TxnState::Aborted);
772 }
773
774 #[test]
775 fn broker_transaction_conflict_maps_to_conflict_error() {
776 let mut client = TxnClient::new(0);
777 let _ = client.new_txn(1, 0);
778 let err = client
779 .handle_new_txn_response(pb::CommandNewTxnResponse {
780 request_id: 1,
781 txnid_most_bits: None,
782 txnid_least_bits: None,
783 error: Some(pb::ServerError::TransactionConflict as i32),
784 message: Some("concurrent txn".to_owned()),
785 })
786 .expect_err("conflict");
787 assert!(matches!(err, TxnError::Conflict));
788 assert!(client.is_empty());
790 }
791
792 #[test]
793 fn broker_transaction_not_found_maps_to_not_found_error() {
794 let mut client = TxnClient::new(0);
795 let _ = client.new_txn(1, 0);
796 let id = client
797 .handle_new_txn_response(ok_new_txn_response(1, 0, 4))
798 .unwrap()
799 .unwrap();
800 let _ = client.end_txn(2, id, TxnAction::Commit);
801
802 let err = client
803 .handle_end_txn_response(pb::CommandEndTxnResponse {
804 request_id: 2,
805 txnid_least_bits: Some(4),
806 txnid_most_bits: Some(0),
807 error: Some(pb::ServerError::TransactionNotFound as i32),
808 message: Some("gc'd".to_owned()),
809 })
810 .expect_err("not found");
811 assert!(matches!(err, TxnError::NotFound));
812 assert_eq!(client.transaction(id).unwrap().state, TxnState::Errored);
813 }
814
815 #[test]
816 fn unknown_broker_code_falls_through_to_broker_variant() {
817 let mut client = TxnClient::new(0);
818 let _ = client.new_txn(1, 0);
819 let err = client
820 .handle_new_txn_response(pb::CommandNewTxnResponse {
821 request_id: 1,
822 txnid_most_bits: None,
823 txnid_least_bits: None,
824 error: Some(pb::ServerError::PersistenceError as i32),
825 message: Some("bookie down".to_owned()),
826 })
827 .expect_err("broker");
828 match err {
829 TxnError::Broker(code, msg) => {
830 assert_eq!(code, pb::ServerError::PersistenceError as i32);
831 assert_eq!(msg, "bookie down");
832 }
833 other => panic!("expected Broker variant, got {other:?}"),
834 }
835 }
836
837 #[test]
838 fn forget_drops_metadata() {
839 let mut client = TxnClient::new(0);
840 let _ = client.new_txn(1, 0);
841 let id = client
842 .handle_new_txn_response(ok_new_txn_response(1, 0, 1))
843 .unwrap()
844 .unwrap();
845 assert!(client.transaction(id).is_some());
846 client.forget(id);
847 assert!(client.transaction(id).is_none());
848 }
849
850 #[test]
855 fn handle_new_txn_response_drops_unknown_request_id() {
856 let mut client = TxnClient::new(0);
857 let result = client.handle_new_txn_response(ok_new_txn_response(42, 0, 1));
860 assert!(matches!(result, Ok(None)));
861 assert!(client.is_empty());
863 }
864
865 #[test]
870 fn txn_error_invalid_status_maps_to_aborted() {
871 let err = TxnError::from_broker(pb::ServerError::InvalidTxnStatus as i32, "ended".into());
872 assert!(matches!(err, TxnError::Aborted));
873 }
874
875 #[test]
880 fn txn_error_tc_not_found_maps_to_not_found() {
881 let err = TxnError::from_broker(
882 pb::ServerError::TransactionCoordinatorNotFound as i32,
883 "gc'd".into(),
884 );
885 assert!(matches!(err, TxnError::NotFound));
886 let err2 =
888 TxnError::from_broker(pb::ServerError::TransactionNotFound as i32, "gc'd".into());
889 assert!(matches!(err2, TxnError::NotFound));
890 }
891
892 #[test]
896 fn txn_id_display_uses_colon_separator() {
897 let id = TxnId::new(7, 42);
898 assert_eq!(format!("{id}"), "7:42");
899 assert_eq!(id, TxnId::new(7, 42));
901 }
902
903 #[test]
909 fn add_partition_broker_error_marks_errored_and_skips_topic() {
910 let mut client = TxnClient::new(0);
911 let _ = client.new_txn(1, 0);
912 let id = client
913 .handle_new_txn_response(ok_new_txn_response(1, 0, 5))
914 .unwrap()
915 .unwrap();
916 let _ = client.add_partition(2, id, "persistent://p/n/t".to_owned());
917
918 let err = client
919 .handle_add_partition_response(pb::CommandAddPartitionToTxnResponse {
920 request_id: 2,
921 txnid_least_bits: Some(5),
922 txnid_most_bits: Some(0),
923 error: Some(pb::ServerError::PersistenceError as i32),
924 message: Some("bookie down".to_owned()),
925 })
926 .expect_err("broker error");
927 assert!(matches!(err, TxnError::Broker(..)));
928
929 let meta = client.transaction(id).expect("txn still tracked");
930 assert_eq!(meta.state, TxnState::Errored);
931 assert!(
932 meta.produced_topics.is_empty(),
933 "topic must NOT be recorded on broker error"
934 );
935 }
936
937 #[test]
941 fn add_subscription_broker_error_marks_errored_and_skips_subscription() {
942 let mut client = TxnClient::new(0);
943 let _ = client.new_txn(1, 0);
944 let id = client
945 .handle_new_txn_response(ok_new_txn_response(1, 0, 6))
946 .unwrap()
947 .unwrap();
948 let _ = client.add_subscription(2, id, "sub-x".to_owned(), "persistent://p/n/t".to_owned());
949
950 let err = client
951 .handle_add_subscription_response(pb::CommandAddSubscriptionToTxnResponse {
952 request_id: 2,
953 txnid_least_bits: Some(6),
954 txnid_most_bits: Some(0),
955 error: Some(pb::ServerError::TransactionConflict as i32),
956 message: Some("conflict".to_owned()),
957 })
958 .expect_err("broker error");
959 assert!(matches!(err, TxnError::Conflict));
960
961 let meta = client.transaction(id).expect("txn still tracked");
962 assert_eq!(meta.state, TxnState::Errored);
963 assert!(
964 meta.acked_subscriptions.is_empty(),
965 "subscription must NOT be recorded on broker error"
966 );
967 }
968}