1use std::{fmt, str, str::FromStr};
43
44use bytes::{BufMut, Bytes, BytesMut};
45
46pub const COORDINATION_STATE_TOPIC: &str = "__coordination_state";
48
49pub const MAX_ROLE_LEN: usize = 249;
54
55pub const MAX_MEMBER_ID_LEN: usize = 249;
57
58const RECORD_VERSION: i16 = 0;
60
61const KEY_PART: &str = "key";
64const REGISTRATION_VALUE_PART: &str = "registration value";
65const LEASE_VALUE_PART: &str = "lease value";
66
67const ROLE_FIELD: &str = "role";
70const MEMBER_FIELD: &str = "member id";
71
72#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
78#[non_exhaustive]
79pub enum RecordError {
80 #[error("malformed coordination state {part}: {message}")]
82 Format {
83 part: &'static str,
86 message: String,
88 },
89
90 #[error("a coordination {field} must not be empty")]
92 Empty {
93 field: &'static str,
95 },
96
97 #[error("a coordination {field} of {length} bytes is longer than the {bound}-byte maximum")]
99 TooLong {
100 field: &'static str,
102 length: usize,
104 bound: usize,
106 },
107
108 #[error("a fencing token must not be negative, got {producer_id}:{producer_epoch}")]
111 NegativeFencingToken {
112 producer_id: i64,
114 producer_epoch: i16,
116 },
117
118 #[error("expected a fencing token of the form producer_id:producer_epoch, got {text:?}")]
121 FencingTokenForm {
122 text: String,
124 },
125}
126
127#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
137pub struct Role {
138 name: String,
139 wire_length: i16,
141}
142
143impl Role {
144 pub fn new(role: &str) -> Result<Self, RecordError> {
152 let wire_length = check_name(role, ROLE_FIELD, MAX_ROLE_LEN)?;
153 Ok(Self {
154 name: role.to_owned(),
155 wire_length,
156 })
157 }
158
159 #[must_use]
161 pub fn as_str(&self) -> &str {
162 &self.name
163 }
164}
165
166impl fmt::Display for Role {
167 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
168 formatter.write_str(&self.name)
169 }
170}
171
172#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
181pub struct MemberId {
182 id: String,
183 wire_length: i16,
185}
186
187impl MemberId {
188 pub fn new(member: &str) -> Result<Self, RecordError> {
196 let wire_length = check_name(member, MEMBER_FIELD, MAX_MEMBER_ID_LEN)?;
197 Ok(Self {
198 id: member.to_owned(),
199 wire_length,
200 })
201 }
202
203 #[must_use]
205 pub fn as_str(&self) -> &str {
206 &self.id
207 }
208}
209
210impl fmt::Display for MemberId {
211 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
212 formatter.write_str(&self.id)
213 }
214}
215
216fn check_name(name: &str, field: &'static str, bound: usize) -> Result<i16, RecordError> {
219 if name.is_empty() {
220 return Err(RecordError::Empty { field });
221 }
222 let too_long = || RecordError::TooLong {
223 field,
224 length: name.len(),
225 bound,
226 };
227 if name.len() > bound {
228 return Err(too_long());
229 }
230 i16::try_from(name.len()).map_err(|_error| too_long())
231}
232
233#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
247pub struct FencingToken {
248 producer_id: i64,
249 producer_epoch: i16,
250}
251
252impl FencingToken {
253 pub fn new(producer_id: i64, producer_epoch: i16) -> Result<Self, RecordError> {
261 if producer_id < 0 || producer_epoch < 0 {
262 return Err(RecordError::NegativeFencingToken {
263 producer_id,
264 producer_epoch,
265 });
266 }
267 Ok(Self {
268 producer_id,
269 producer_epoch,
270 })
271 }
272
273 #[must_use]
275 pub const fn producer_id(self) -> i64 {
276 self.producer_id
277 }
278
279 #[must_use]
281 pub const fn producer_epoch(self) -> i16 {
282 self.producer_epoch
283 }
284}
285
286impl fmt::Display for FencingToken {
287 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
288 write!(formatter, "{}:{}", self.producer_id, self.producer_epoch)
289 }
290}
291
292impl FromStr for FencingToken {
293 type Err = RecordError;
294
295 fn from_str(serialized: &str) -> Result<Self, Self::Err> {
296 let form = || RecordError::FencingTokenForm {
297 text: serialized.to_owned(),
298 };
299 let (producer_id, producer_epoch) = serialized.split_once(':').ok_or_else(form)?;
300 if producer_epoch.contains(':') {
301 return Err(form());
302 }
303 Self::new(
304 producer_id.parse().map_err(|_error| form())?,
305 producer_epoch.parse().map_err(|_error| form())?,
306 )
307 }
308}
309
310#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
312pub enum RecordKind {
313 Registration,
315 Lease,
317}
318
319impl RecordKind {
320 #[must_use]
322 pub const fn code(self) -> i16 {
323 match self {
324 Self::Registration => 0,
325 Self::Lease => 1,
326 }
327 }
328
329 pub fn from_code(code: i16) -> Result<Self, RecordError> {
335 match code {
336 0 => Ok(Self::Registration),
337 1 => Ok(Self::Lease),
338 other => Err(RecordError::Format {
339 part: KEY_PART,
340 message: format!("unknown coordination state record kind {other}"),
341 }),
342 }
343 }
344}
345
346#[derive(Debug, Clone, PartialEq, Eq, Hash)]
354pub struct CoordinationKey {
355 pub kind: RecordKind,
357 pub role: Role,
359 pub member: Option<MemberId>,
361}
362
363impl CoordinationKey {
364 #[must_use]
366 pub fn registration(role: Role, member: MemberId) -> Self {
367 Self {
368 kind: RecordKind::Registration,
369 role,
370 member: Some(member),
371 }
372 }
373
374 #[must_use]
376 pub fn lease(role: Role) -> Self {
377 Self {
378 kind: RecordKind::Lease,
379 role,
380 member: None,
381 }
382 }
383}
384
385#[derive(Debug, Clone, PartialEq, Eq, Hash)]
390pub struct Registration {
391 pub member: MemberId,
393 pub registered_at: i64,
395}
396
397#[derive(Debug, Clone, PartialEq, Eq, Hash)]
404pub struct Lease {
405 pub member: MemberId,
407 pub token: FencingToken,
409 pub granted_at: i64,
412 pub deadline: i64,
414}
415
416#[derive(Debug, Clone, PartialEq, Eq, Hash)]
418pub enum CoordinationRecord {
419 Registration(Registration),
421 Lease(Lease),
423 Tombstone,
426}
427
428#[must_use]
433pub fn encode_key(key: &CoordinationKey) -> Bytes {
434 let (member, member_length) = match key.kind {
435 RecordKind::Registration => key
436 .member
437 .as_ref()
438 .map_or(("", 0), |member| (member.as_str(), member.wire_length)),
439 RecordKind::Lease => ("", 0),
440 };
441 let role = key.role.as_str();
442 let mut buffer = BytesMut::with_capacity(8 + role.len() + member.len());
443 buffer.put_i16(RECORD_VERSION);
444 buffer.put_i16(key.kind.code());
445 put_string(&mut buffer, role, key.role.wire_length);
446 put_string(&mut buffer, member, member_length);
447 buffer.freeze()
448}
449
450#[must_use]
455pub fn encode_value(record: &CoordinationRecord) -> Option<Bytes> {
456 match record {
457 CoordinationRecord::Registration(registration) => {
458 let member = ®istration.member;
459 let mut buffer = BytesMut::with_capacity(12 + member.as_str().len());
460 buffer.put_i16(RECORD_VERSION);
461 put_string(&mut buffer, member.as_str(), member.wire_length);
462 buffer.put_i64(registration.registered_at);
463 Some(buffer.freeze())
464 }
465 CoordinationRecord::Lease(lease) => {
466 let member = &lease.member;
467 let mut buffer = BytesMut::with_capacity(30 + member.as_str().len());
468 buffer.put_i16(RECORD_VERSION);
469 put_string(&mut buffer, member.as_str(), member.wire_length);
470 buffer.put_i64(lease.token.producer_id());
471 buffer.put_i16(lease.token.producer_epoch());
472 buffer.put_i64(lease.granted_at);
473 buffer.put_i64(lease.deadline);
474 Some(buffer.freeze())
475 }
476 CoordinationRecord::Tombstone => None,
477 }
478}
479
480pub fn decode_key(bytes: &[u8]) -> Result<CoordinationKey, RecordError> {
491 let mut reader = Reader::new(bytes, KEY_PART);
492 reader.version()?;
493 let kind = RecordKind::from_code(reader.i16()?)?;
494 let role = reader.string()?;
495 let member = reader.string()?;
496 reader.finish()?;
497 let member = match kind {
498 RecordKind::Registration => Some(MemberId::new(member)?),
499 RecordKind::Lease => {
500 if !member.is_empty() {
501 return Err(reader.malformed(format!(
502 "a lease key carries the empty member string, got {member:?}"
503 )));
504 }
505 None
506 }
507 };
508 Ok(CoordinationKey {
509 kind,
510 role: Role::new(role)?,
511 member,
512 })
513}
514
515pub fn decode_value(
529 kind: RecordKind,
530 bytes: Option<&[u8]>,
531) -> Result<CoordinationRecord, RecordError> {
532 let Some(bytes) = bytes else {
533 return Ok(CoordinationRecord::Tombstone);
534 };
535 match kind {
536 RecordKind::Registration => {
537 decode_registration(bytes).map(CoordinationRecord::Registration)
538 }
539 RecordKind::Lease => decode_lease(bytes).map(CoordinationRecord::Lease),
540 }
541}
542
543fn decode_registration(bytes: &[u8]) -> Result<Registration, RecordError> {
544 let mut reader = Reader::new(bytes, REGISTRATION_VALUE_PART);
545 reader.version()?;
546 let member = reader.string()?;
547 let registered_at = reader.i64()?;
548 reader.finish()?;
549 Ok(Registration {
550 member: MemberId::new(member)?,
551 registered_at,
552 })
553}
554
555fn decode_lease(bytes: &[u8]) -> Result<Lease, RecordError> {
556 let mut reader = Reader::new(bytes, LEASE_VALUE_PART);
557 reader.version()?;
558 let member = reader.string()?;
559 let producer_id = reader.i64()?;
560 let producer_epoch = reader.i16()?;
561 let granted_at = reader.i64()?;
562 let deadline = reader.i64()?;
563 reader.finish()?;
564 Ok(Lease {
565 member: MemberId::new(member)?,
566 token: FencingToken::new(producer_id, producer_epoch)?,
567 granted_at,
568 deadline,
569 })
570}
571
572fn put_string(buffer: &mut BytesMut, value: &str, length: i16) {
577 buffer.put_i16(length);
578 buffer.put_slice(value.as_bytes());
579}
580
581struct Reader<'a> {
588 data: &'a [u8],
589 part: &'static str,
590}
591
592impl<'a> Reader<'a> {
593 fn new(data: &'a [u8], part: &'static str) -> Self {
594 Self { data, part }
595 }
596
597 fn malformed(&self, message: String) -> RecordError {
598 RecordError::Format {
599 part: self.part,
600 message,
601 }
602 }
603
604 fn truncated(&self) -> RecordError {
605 self.malformed(format!("truncated coordination state {}", self.part))
606 }
607
608 fn finish(&self) -> Result<(), RecordError> {
610 if self.data.is_empty() {
611 return Ok(());
612 }
613 Err(self.malformed(format!(
614 "trailing bytes in coordination state {}",
615 self.part
616 )))
617 }
618
619 fn take_array<const N: usize>(&mut self) -> Result<[u8; N], RecordError> {
620 let Some((head, rest)) = self.data.split_first_chunk::<N>() else {
621 return Err(self.truncated());
622 };
623 self.data = rest;
624 Ok(*head)
625 }
626
627 fn take(&mut self, count: usize) -> Result<&'a [u8], RecordError> {
628 let Some((head, rest)) = self.data.split_at_checked(count) else {
629 return Err(self.truncated());
630 };
631 self.data = rest;
632 Ok(head)
633 }
634
635 fn i16(&mut self) -> Result<i16, RecordError> {
636 Ok(i16::from_be_bytes(self.take_array()?))
637 }
638
639 fn i64(&mut self) -> Result<i64, RecordError> {
640 Ok(i64::from_be_bytes(self.take_array()?))
641 }
642
643 fn string(&mut self) -> Result<&'a str, RecordError> {
646 let length = self.i16()?;
647 let length = usize::try_from(length).map_err(|_error| {
648 self.malformed(format!(
649 "negative string length {length} in coordination state {}",
650 self.part
651 ))
652 })?;
653 let bytes = self.take(length)?;
654 str::from_utf8(bytes).map_err(|_error| {
655 self.malformed(format!(
656 "non-UTF-8 string in coordination state {}",
657 self.part
658 ))
659 })
660 }
661
662 fn version(&mut self) -> Result<(), RecordError> {
664 let version = self.i16()?;
665 if version == RECORD_VERSION {
666 return Ok(());
667 }
668 Err(self.malformed(format!(
669 "unsupported coordination state {} version {version}",
670 self.part
671 )))
672 }
673}
674
675#[cfg(test)]
676mod tests {
677 use assert2::{assert, check};
678 use proptest::{collection, prelude::*};
679
680 use super::*;
681
682 const NAME: &str = "[a-zA-Z0-9._-]{1,64}";
685
686 fn role(name: &str) -> Role {
687 Role::new(name).unwrap()
688 }
689
690 fn member(name: &str) -> MemberId {
691 MemberId::new(name).unwrap()
692 }
693
694 #[test]
698 fn a_role_and_a_member_render_as_their_own_names() {
699 check!(role("controller").to_string() == "controller");
700 check!(member("node-1").to_string() == "node-1");
701 check!(
702 format!("{} holds {}", member("node-1"), role("controller"))
703 == "node-1 holds controller"
704 );
705 }
706
707 mod golden {
713 use super::*;
714
715 const ROLE: &str = "controller";
716 const MEMBER: &str = "node-1";
717 const REGISTERED_AT: i64 = 1_700_000_000_000;
718 const GRANTED_AT: i64 = 1_700_000_000_000;
719 const DEADLINE: i64 = 1_700_000_030_000;
720 const PRODUCER_ID: i64 = 4242;
721 const PRODUCER_EPOCH: i16 = 7;
722
723 pub(super) const REGISTRATION_KEY: [u8; 24] = [
726 0x00, 0x00, 0x00, 0x00, 0x00, 0x0A, 0x63, 0x6F, 0x6E, 0x74, 0x72, 0x6F, 0x6C, 0x6C, 0x65, 0x72, 0x00, 0x06, 0x6E, 0x6F, 0x64, 0x65, 0x2D, 0x31, ];
733
734 pub(super) const LEASE_KEY: [u8; 18] = [
736 0x00, 0x00, 0x00, 0x01, 0x00, 0x0A, 0x63, 0x6F, 0x6E, 0x74, 0x72, 0x6F, 0x6C, 0x6C, 0x65, 0x72, 0x00, 0x00, ];
742
743 pub(super) const REGISTRATION_VALUE: [u8; 18] = [
745 0x00, 0x00, 0x00, 0x06, 0x6E, 0x6F, 0x64, 0x65, 0x2D, 0x31, 0x00, 0x00, 0x01, 0x8B, 0xCF, 0xE5, 0x68, 0x00, ];
750
751 pub(super) const LEASE_VALUE: [u8; 36] = [
754 0x00, 0x00, 0x00, 0x06, 0x6E, 0x6F, 0x64, 0x65, 0x2D, 0x31, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x92, 0x00, 0x07, 0x00, 0x00, 0x01, 0x8B, 0xCF, 0xE5, 0x68, 0x00, 0x00, 0x00, 0x01, 0x8B, 0xCF, 0xE5, 0xDD, 0x30, ];
762
763 fn registration_key() -> CoordinationKey {
764 CoordinationKey::registration(role(ROLE), member(MEMBER))
765 }
766
767 fn lease_key() -> CoordinationKey {
768 CoordinationKey::lease(role(ROLE))
769 }
770
771 fn registration_value() -> CoordinationRecord {
772 CoordinationRecord::Registration(Registration {
773 member: member(MEMBER),
774 registered_at: REGISTERED_AT,
775 })
776 }
777
778 fn lease_value() -> CoordinationRecord {
779 CoordinationRecord::Lease(Lease {
780 member: member(MEMBER),
781 token: FencingToken::new(PRODUCER_ID, PRODUCER_EPOCH).unwrap(),
782 granted_at: GRANTED_AT,
783 deadline: DEADLINE,
784 })
785 }
786
787 #[test]
788 fn a_registration_key_encodes_to_the_frozen_bytes() {
789 check!(encode_key(®istration_key()).as_ref() == REGISTRATION_KEY.as_slice());
790 check!(decode_key(®ISTRATION_KEY).unwrap() == registration_key());
791 }
792
793 #[test]
794 fn a_lease_key_encodes_to_the_frozen_bytes() {
795 check!(encode_key(&lease_key()).as_ref() == LEASE_KEY.as_slice());
796 check!(decode_key(&LEASE_KEY).unwrap() == lease_key());
797 }
798
799 #[test]
800 fn a_registration_value_encodes_to_the_frozen_bytes() {
801 let encoded = encode_value(®istration_value()).unwrap();
802 check!(encoded.as_ref() == REGISTRATION_VALUE.as_slice());
803 let decoded =
804 decode_value(RecordKind::Registration, Some(®ISTRATION_VALUE)).unwrap();
805 check!(decoded == registration_value());
806 }
807
808 #[test]
809 fn a_lease_value_encodes_to_the_frozen_bytes() {
810 let encoded = encode_value(&lease_value()).unwrap();
811 check!(encoded.as_ref() == LEASE_VALUE.as_slice());
812 let decoded = decode_value(RecordKind::Lease, Some(&LEASE_VALUE)).unwrap();
813 check!(decoded == lease_value());
814 }
815 }
816
817 #[test]
818 fn the_topic_name_is_frozen() {
819 check!(COORDINATION_STATE_TOPIC == "__coordination_state");
820 }
821
822 #[test]
823 fn a_record_kind_maps_onto_its_wire_code() {
824 for kind in [RecordKind::Registration, RecordKind::Lease] {
825 check!(RecordKind::from_code(kind.code()).unwrap() == kind);
826 }
827 check!(RecordKind::Registration.code() == 0);
828 check!(RecordKind::Lease.code() == 1);
829 for code in [-1_i16, 2, 9, i16::MAX, i16::MIN] {
830 check!(RecordKind::from_code(code).is_err(), "kind {code}");
831 }
832 }
833
834 #[test]
835 fn a_tombstone_encodes_to_a_null_value() {
836 check!(encode_value(&CoordinationRecord::Tombstone).is_none());
837 for kind in [RecordKind::Registration, RecordKind::Lease] {
838 check!(decode_value(kind, None).unwrap() == CoordinationRecord::Tombstone);
839 }
840 }
841
842 #[test]
843 fn a_lease_key_ignores_a_member_the_caller_set() {
844 let key = CoordinationKey {
845 kind: RecordKind::Lease,
846 role: role("controller"),
847 member: Some(member("node-1")),
848 };
849 check!(
850 decode_key(&encode_key(&key)).unwrap() == CoordinationKey::lease(role("controller"))
851 );
852 }
853
854 #[test]
855 fn a_role_rejects_an_empty_name_and_a_long_name() {
856 assert!(let Err(RecordError::Empty { field }) = Role::new(""));
857 check!(field == "role");
858 let long = "r".repeat(MAX_ROLE_LEN + 1);
859 assert!(let Err(RecordError::TooLong { length, bound, .. }) = Role::new(&long));
860 check!(length == MAX_ROLE_LEN + 1);
861 check!(bound == MAX_ROLE_LEN);
862 check!(Role::new(&"r".repeat(MAX_ROLE_LEN)).is_ok());
863 }
864
865 #[test]
866 fn a_member_id_rejects_an_empty_name_and_a_long_name() {
867 assert!(let Err(RecordError::Empty { field }) = MemberId::new(""));
868 check!(field == "member id");
869 let long = "m".repeat(MAX_MEMBER_ID_LEN + 1);
870 check!(MemberId::new(&long).is_err());
871 check!(MemberId::new(&"m".repeat(MAX_MEMBER_ID_LEN)).is_ok());
872 }
873
874 #[test]
875 fn a_fencing_token_orders_on_the_producer_id_first() {
876 let fresh = FencingToken::new(5, 0).unwrap();
877 let wrapped = FencingToken::new(4, i16::MAX).unwrap();
878 check!(fresh > wrapped);
879 check!(FencingToken::new(4, 1).unwrap() > FencingToken::new(4, 0).unwrap());
880 check!(FencingToken::new(4, 0).unwrap() == FencingToken::new(4, 0).unwrap());
881 }
882
883 #[test]
884 fn a_fencing_token_rejects_a_negative_component() {
885 check!(FencingToken::new(-1, 0).is_err());
886 check!(FencingToken::new(0, -1).is_err());
887 check!(FencingToken::new(0, 0).is_ok());
888 }
889
890 #[test]
891 fn a_fencing_token_round_trips_through_its_string_form() {
892 let token = FencingToken::new(4242, 7).unwrap();
893 check!(token.to_string() == "4242:7");
894 check!("4242:7".parse::<FencingToken>().unwrap() == token);
895 for text in ["", "4242", "4242:7:1", "a:7", "4242:b", "-1:7", "4242:-1"] {
896 check!(text.parse::<FencingToken>().is_err(), "token {text:?}");
897 }
898 }
899
900 #[test]
902 fn a_malformed_key_returns_an_error() {
903 let mut trailing = golden::REGISTRATION_KEY.to_vec();
904 trailing.push(0x00);
905 let cases: [(&str, Vec<u8>); 10] = [
906 ("empty buffer", vec![]),
907 ("truncated version", vec![0x00]),
908 ("truncated kind", vec![0x00, 0x00, 0x00]),
909 (
910 "unknown version",
911 vec![0x00, 0x01, 0x00, 0x00, 0x00, 0x01, b'a', 0x00, 0x01, b'm'],
912 ),
913 (
914 "unknown kind",
915 vec![0x00, 0x00, 0x00, 0x09, 0x00, 0x01, b'a', 0x00, 0x00],
916 ),
917 (
918 "negative role length",
919 vec![0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00],
920 ),
921 (
922 "role length past the buffer",
923 vec![0x00, 0x00, 0x00, 0x00, 0x7F, 0xFF, b'a'],
924 ),
925 (
926 "non-UTF-8 role",
927 vec![0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xFF, 0x00, 0x01, b'm'],
928 ),
929 (
930 "registration key with an empty member",
931 vec![0x00, 0x00, 0x00, 0x00, 0x00, 0x01, b'a', 0x00, 0x00],
932 ),
933 ("trailing bytes", trailing),
934 ];
935 for (name, bytes) in cases {
936 check!(decode_key(&bytes).is_err(), "key case {name}");
937 }
938 }
939
940 #[test]
941 fn a_lease_key_that_names_a_member_returns_an_error() {
942 let bytes = vec![
943 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, b'a', 0x00, 0x01, b'm', ];
948 assert!(let Err(RecordError::Format { part, .. }) = decode_key(&bytes));
949 check!(part == "key");
950 }
951
952 #[test]
954 fn a_malformed_value_returns_an_error() {
955 let mut trailing_registration = golden::REGISTRATION_VALUE.to_vec();
956 trailing_registration.push(0x00);
957 let mut trailing_lease = golden::LEASE_VALUE.to_vec();
958 trailing_lease.push(0x00);
959 let cases: [(&str, RecordKind, Vec<u8>); 9] = [
960 ("empty registration", RecordKind::Registration, vec![]),
961 (
962 "unknown registration version",
963 RecordKind::Registration,
964 vec![0x00, 0x01, 0x00, 0x01, b'm', 0, 0, 0, 0, 0, 0, 0, 1],
965 ),
966 (
967 "negative registration member length",
968 RecordKind::Registration,
969 vec![0x00, 0x00, 0xFF, 0xFF, 0, 0, 0, 0, 0, 0, 0, 1],
970 ),
971 (
972 "truncated registration timestamp",
973 RecordKind::Registration,
974 vec![0x00, 0x00, 0x00, 0x01, b'm', 0x00],
975 ),
976 (
977 "trailing bytes after a registration",
978 RecordKind::Registration,
979 trailing_registration,
980 ),
981 ("empty lease", RecordKind::Lease, vec![]),
982 (
983 "truncated lease deadline",
984 RecordKind::Lease,
985 vec![
986 0x00, 0x00, 0x00, 0x01, b'm', 0, 0, 0, 0, 0, 0, 0, 1, 0x00, 0x00, 0, 0, 0, 0,
987 0, 0, 0, 1,
988 ],
989 ),
990 (
991 "negative lease producer id",
992 RecordKind::Lease,
993 vec![
994 0x00, 0x00, 0x00, 0x01, b'm', 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
995 0x00, 0x00, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 2,
996 ],
997 ),
998 (
999 "trailing bytes after a lease",
1000 RecordKind::Lease,
1001 trailing_lease,
1002 ),
1003 ];
1004 for (name, kind, bytes) in cases {
1005 check!(
1006 decode_value(kind, Some(&bytes)).is_err(),
1007 "value case {name}"
1008 );
1009 }
1010 }
1011
1012 proptest! {
1013 #[test]
1014 fn a_registration_key_survives_a_round_trip(name in NAME, id in NAME) {
1015 let key = CoordinationKey::registration(role(&name), member(&id));
1016 let decoded = decode_key(&encode_key(&key)).unwrap();
1017 check!(decoded == key);
1018 }
1019
1020 #[test]
1021 fn a_lease_key_survives_a_round_trip(name in NAME) {
1022 let key = CoordinationKey::lease(role(&name));
1023 let decoded = decode_key(&encode_key(&key)).unwrap();
1024 check!(decoded == key);
1025 }
1026
1027 #[test]
1028 fn a_registration_value_survives_a_round_trip(
1029 id in NAME,
1030 registered_at in any::<i64>(),
1031 ) {
1032 let record = CoordinationRecord::Registration(Registration {
1033 member: member(&id),
1034 registered_at,
1035 });
1036 let encoded = encode_value(&record).unwrap();
1037 let decoded = decode_value(RecordKind::Registration, Some(&encoded)).unwrap();
1038 check!(decoded == record);
1039 }
1040
1041 #[test]
1042 fn a_lease_value_survives_a_round_trip(
1043 id in NAME,
1044 producer_id in 0_i64..=i64::MAX,
1045 producer_epoch in 0_i16..=i16::MAX,
1046 granted_at in any::<i64>(),
1047 deadline in any::<i64>(),
1048 ) {
1049 let record = CoordinationRecord::Lease(Lease {
1050 member: member(&id),
1051 token: FencingToken::new(producer_id, producer_epoch).unwrap(),
1052 granted_at,
1053 deadline,
1054 });
1055 let encoded = encode_value(&record).unwrap();
1056 let decoded = decode_value(RecordKind::Lease, Some(&encoded)).unwrap();
1057 check!(decoded == record);
1058 }
1059
1060 #[test]
1062 fn a_decoder_never_panics_on_arbitrary_bytes(
1063 bytes in collection::vec(any::<u8>(), 0..64),
1064 ) {
1065 let _ = decode_key(&bytes);
1066 let _ = decode_value(RecordKind::Registration, Some(&bytes));
1067 let _ = decode_value(RecordKind::Lease, Some(&bytes));
1068 }
1069 }
1070}