1use std::fmt;
9
10use crate::header::{GuaranteeSet, HeaderError, Hello};
11
12pub const CAPABILITY_DATAGRAM: u64 = 1;
16
17pub const CAPABILITY_PATH_REPORT: u64 = 2;
21
22#[derive(Clone, Copy, Debug, PartialEq, Eq)]
24pub struct Agreed {
25 pub version: u64,
27 pub send_max_header_bytes: u64,
30 pub guarantees: GuaranteeSet,
36 pub datagrams: bool,
39 pub path_report: bool,
42}
43
44impl Agreed {
45 pub fn check_header_len(&self, len: usize) -> Result<(), HeaderError> {
52 if len as u64 > self.send_max_header_bytes {
53 return Err(HeaderError::HeaderTooLong {
54 len,
55 max: self.send_max_header_bytes,
56 });
57 }
58 Ok(())
59 }
60}
61
62#[derive(Clone, Copy, Debug, PartialEq, Eq)]
64pub enum NegotiateError {
65 NoCommonVersion,
67 UnsupportedRequiredCapability(u64),
69 IncomparableGuarantee {
72 dimension: &'static str,
74 },
75 GuaranteeNotOffered {
79 peers_requirement: bool,
81 },
82}
83
84impl fmt::Display for NegotiateError {
85 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
86 match self {
87 NegotiateError::NoCommonVersion => {
88 f.write_str("no wire protocol version is supported by both peers")
89 }
90 NegotiateError::UnsupportedRequiredCapability(c) => {
91 write!(f, "peer requires unsupported capability {c}")
92 }
93 NegotiateError::IncomparableGuarantee { dimension } => {
94 write!(
95 f,
96 "the two peers declare different {dimension}, which is not an ordered dimension"
97 )
98 }
99 NegotiateError::GuaranteeNotOffered { peers_requirement } => {
100 let who = if *peers_requirement { "peer" } else { "local" };
101 write!(
102 f,
103 "the effective guarantee set does not reach the {who} requirement"
104 )
105 }
106 }
107 }
108}
109
110impl std::error::Error for NegotiateError {}
111
112impl From<NegotiateError> for weida_core::Error {
113 fn from(e: NegotiateError) -> weida_core::Error {
114 weida_core::Error::Negotiation(e.to_string())
115 }
116}
117
118pub fn negotiate(ours: &Hello, theirs: &Hello) -> Result<Agreed, NegotiateError> {
131 let version = theirs
132 .versions
133 .iter()
134 .copied()
135 .filter(|v| ours.versions.contains(v))
136 .max()
137 .ok_or(NegotiateError::NoCommonVersion)?;
138
139 if let Some(missing) = theirs
140 .required_capabilities
141 .iter()
142 .copied()
143 .find(|c| !ours.capabilities.contains(c))
144 {
145 return Err(NegotiateError::UnsupportedRequiredCapability(missing));
146 }
147
148 let guarantees = ours
149 .offered()
150 .intersect(&theirs.offered())
151 .map_err(|dimension| NegotiateError::IncomparableGuarantee { dimension })?;
152 for (required, peers_requirement) in [(theirs.required(), true), (ours.required(), false)] {
153 if !guarantees.reaches(&required) {
154 return Err(NegotiateError::GuaranteeNotOffered { peers_requirement });
155 }
156 }
157
158 Ok(Agreed {
159 version,
160 send_max_header_bytes: theirs.max_header_bytes,
161 guarantees,
162 datagrams: ours.capabilities.contains(&CAPABILITY_DATAGRAM)
163 && theirs.capabilities.contains(&CAPABILITY_DATAGRAM),
164 path_report: ours.capabilities.contains(&CAPABILITY_PATH_REPORT)
165 && theirs.capabilities.contains(&CAPABILITY_PATH_REPORT),
166 })
167}
168
169#[cfg(test)]
170mod tests {
171 use super::*;
172
173 use crate::header::{
174 Acknowledgement, Backpressure, Deduplication, Durability, OrderingMode, ProducerNaming,
175 };
176
177 fn hello(versions: &[u64], caps: &[u64], required: &[u64], max_header: u64) -> Hello {
178 Hello {
179 versions: versions.to_vec(),
180 max_header_bytes: max_header,
181 max_transfers: 1024,
182 capabilities: caps.to_vec(),
183 required_capabilities: required.to_vec(),
184 guarantees_offered: None,
185 guarantees_required: None,
186 }
187 }
188
189 fn declaring(offered: GuaranteeSet, required: GuaranteeSet) -> Hello {
191 Hello {
192 guarantees_offered: Some(offered),
193 guarantees_required: Some(required),
194 ..Hello::v0(16384, 1024)
195 }
196 }
197
198 #[test]
199 fn v0_peers_agree_on_version_zero() {
200 let ours = Hello::v0(16384, 1024);
201 let theirs = Hello::v0(4096, 512);
202 let agreed = negotiate(&ours, &theirs).unwrap();
203 assert_eq!(agreed.version, 0);
204 assert_eq!(agreed.send_max_header_bytes, 4096);
206 }
207
208 #[test]
209 fn a_header_longer_than_the_peer_accepts_is_refused() {
210 let agreed = negotiate(&Hello::v0(16384, 1024), &Hello::v0(4096, 512)).unwrap();
211 assert_eq!(agreed.check_header_len(4096), Ok(()));
212 assert_eq!(
213 agreed.check_header_len(4097),
214 Err(HeaderError::HeaderTooLong {
215 len: 4097,
216 max: 4096
217 })
218 );
219 }
220
221 #[test]
222 fn the_highest_common_version_wins() {
223 let ours = hello(&[0, 1, 2, 5], &[], &[], 16384);
224 let theirs = hello(&[1, 2, 3], &[], &[], 16384);
225 assert_eq!(negotiate(&ours, &theirs).unwrap().version, 2);
226 }
227
228 #[test]
229 fn version_order_in_the_list_does_not_matter() {
230 let ours = hello(&[5, 0, 2], &[], &[], 16384);
231 let theirs = hello(&[2, 5], &[], &[], 16384);
232 assert_eq!(negotiate(&ours, &theirs).unwrap().version, 5);
233 }
234
235 #[test]
236 fn an_empty_intersection_fails() {
237 let ours = hello(&[0], &[], &[], 16384);
238 for theirs in [hello(&[99], &[], &[], 16384), hello(&[], &[], &[], 16384)] {
239 assert_eq!(
240 negotiate(&ours, &theirs),
241 Err(NegotiateError::NoCommonVersion)
242 );
243 }
244 }
245
246 #[test]
247 fn a_required_capability_we_lack_fails() {
248 let ours = Hello::v0(16384, 1024);
249 let theirs = hello(&[0], &[7], &[7], 16384);
250 assert_eq!(
251 negotiate(&ours, &theirs),
252 Err(NegotiateError::UnsupportedRequiredCapability(7))
253 );
254 }
255
256 #[test]
257 fn a_required_capability_we_have_succeeds() {
258 let ours = hello(&[0], &[7, 9], &[], 16384);
259 let theirs = hello(&[0], &[7], &[7], 8192);
260 assert_eq!(
261 negotiate(&ours, &theirs).unwrap(),
262 Agreed {
263 version: 0,
264 send_max_header_bytes: 8192,
265 guarantees: GuaranteeSet::CORE,
266 datagrams: false,
267 path_report: false,
268 }
269 );
270 }
271
272 #[test]
273 fn datagrams_are_agreed_only_when_both_list_the_code() {
274 let with = hello(&[0], &[CAPABILITY_DATAGRAM], &[], 16384);
275 let without = Hello::v0(16384, 1024);
276 assert!(negotiate(&with, &with).unwrap().datagrams);
277 assert!(!negotiate(&with, &without).unwrap().datagrams);
278 assert!(!negotiate(&without, &with).unwrap().datagrams);
279 }
280
281 #[test]
282 fn path_reports_are_agreed_only_when_both_list_the_code() {
283 let with = hello(&[0], &[CAPABILITY_PATH_REPORT], &[], 16384);
284 let without = hello(&[0], &[CAPABILITY_DATAGRAM], &[], 16384);
285 assert!(negotiate(&with, &with).unwrap().path_report);
286 assert!(!negotiate(&with, &without).unwrap().path_report);
287 assert!(!negotiate(&without, &with).unwrap().path_report);
288 }
289
290 #[test]
291 fn two_v0_peers_negotiate_core_unchanged() {
292 let agreed = negotiate(&Hello::v0(16384, 1024), &Hello::v0(16384, 1024)).unwrap();
295 assert_eq!(agreed.guarantees, GuaranteeSet::CORE);
296 assert!(agreed.guarantees.is_core());
297 }
298
299 #[test]
300 fn the_effective_set_is_the_weaker_offer_per_dimension() {
301 let strong = GuaranteeSet {
302 ordering: OrderingMode::PerProducerReassemble,
303 deduplication: Deduplication::Bounded,
304 dedup_window_ms: Some(60_000),
305 control_isolated: true,
306 ..GuaranteeSet::CORE
307 };
308 let weaker = GuaranteeSet {
309 ordering: OrderingMode::PerProducerDetect,
310 deduplication: Deduplication::Bounded,
311 dedup_window_ms: Some(5_000),
312 control_isolated: false,
313 ..GuaranteeSet::CORE
314 };
315 let agreed = negotiate(
316 &declaring(strong, GuaranteeSet::CORE),
317 &declaring(weaker, GuaranteeSet::CORE),
318 )
319 .unwrap();
320 assert_eq!(agreed.guarantees.ordering, OrderingMode::PerProducerDetect);
321 assert_eq!(agreed.guarantees.dedup_window_ms, Some(5_000));
323 assert!(!agreed.guarantees.control_isolated);
324 }
325
326 #[test]
327 fn a_requested_level_the_peer_does_not_offer_fails() {
328 let wants_ordering = GuaranteeSet {
329 ordering: OrderingMode::PerProducerDetect,
330 ..GuaranteeSet::CORE
331 };
332 let err = negotiate(
334 &declaring(wants_ordering, wants_ordering),
335 &Hello::v0(16384, 1024),
336 )
337 .unwrap_err();
338 assert_eq!(
339 err,
340 NegotiateError::GuaranteeNotOffered {
341 peers_requirement: false
342 }
343 );
344 let err = negotiate(
346 &Hello::v0(16384, 1024),
347 &declaring(wants_ordering, wants_ordering),
348 )
349 .unwrap_err();
350 assert_eq!(
351 err,
352 NegotiateError::GuaranteeNotOffered {
353 peers_requirement: true
354 }
355 );
356 }
357
358 #[test]
359 fn unordered_dimensions_must_match_exactly() {
360 for (ours, theirs, dimension) in [
361 (
362 GuaranteeSet {
363 backpressure: Backpressure::Drop,
364 ..GuaranteeSet::CORE
365 },
366 GuaranteeSet::CORE,
367 "backpressure",
368 ),
369 (
370 GuaranteeSet {
371 producer_naming: ProducerNaming::Stable,
372 ..GuaranteeSet::CORE
373 },
374 GuaranteeSet::CORE,
375 "producer naming",
376 ),
377 (
378 GuaranteeSet {
379 acknowledgement: Acknowledgement::Stored,
380 durability: Some(Durability::Flushed),
381 ..GuaranteeSet::CORE
382 },
383 GuaranteeSet {
384 acknowledgement: Acknowledgement::Stored,
385 durability: Some(Durability::Written),
386 ..GuaranteeSet::CORE
387 },
388 "durability",
389 ),
390 ] {
391 let err = negotiate(
392 &declaring(ours, GuaranteeSet::CORE),
393 &declaring(theirs, GuaranteeSet::CORE),
394 )
395 .unwrap_err();
396 assert_eq!(err, NegotiateError::IncomparableGuarantee { dimension });
397 }
398 }
399
400 #[test]
401 fn a_weakened_acknowledgement_drops_the_axes_it_cannot_carry() {
402 let stored = GuaranteeSet {
403 acknowledgement: Acknowledgement::Stored,
404 durability: Some(Durability::Flushed),
405 ..GuaranteeSet::CORE
406 };
407 let agreed = negotiate(
408 &declaring(stored, GuaranteeSet::CORE),
409 &Hello::v0(16384, 1024),
410 )
411 .unwrap();
412 assert_eq!(
415 agreed.guarantees.acknowledgement,
416 Acknowledgement::TransportReceipt
417 );
418 assert_eq!(agreed.guarantees.durability, None);
419 assert!(agreed.guarantees.is_core());
420 }
421
422 #[test]
423 fn an_axis_the_peer_never_declared_is_not_reached() {
424 let stored = |durability| GuaranteeSet {
425 acknowledgement: Acknowledgement::Stored,
426 durability,
427 ..GuaranteeSet::CORE
428 };
429 let replicated = |replicas| GuaranteeSet {
430 acknowledgement: Acknowledgement::Replicated,
431 replicas,
432 ..GuaranteeSet::CORE
433 };
434 for (wanted, undeclared) in [
435 (stored(Some(Durability::Flushed)), stored(None)),
436 (replicated(Some(3)), replicated(None)),
437 ] {
438 let err = negotiate(
439 &declaring(wanted, wanted),
440 &declaring(undeclared, GuaranteeSet::CORE),
441 )
442 .unwrap_err();
443 assert_eq!(
444 err,
445 NegotiateError::GuaranteeNotOffered {
446 peers_requirement: false
447 }
448 );
449 let err = negotiate(
451 &declaring(undeclared, GuaranteeSet::CORE),
452 &declaring(wanted, wanted),
453 )
454 .unwrap_err();
455 assert_eq!(
456 err,
457 NegotiateError::GuaranteeNotOffered {
458 peers_requirement: true
459 }
460 );
461 }
462 }
463
464 #[test]
465 fn an_axis_both_sides_declared_survives_the_intersection() {
466 let stored = GuaranteeSet {
467 acknowledgement: Acknowledgement::Stored,
468 durability: Some(Durability::Flushed),
469 ..GuaranteeSet::CORE
470 };
471 let agreed = negotiate(&declaring(stored, stored), &declaring(stored, stored)).unwrap();
472 assert_eq!(agreed.guarantees, stored);
473 }
474
475 #[test]
476 fn optional_peer_capabilities_are_ignored() {
477 let ours = Hello::v0(16384, 1024);
478 let theirs = hello(&[0], &[1, 2, 3], &[], 16384);
479 assert!(negotiate(&ours, &theirs).is_ok());
480 }
481
482 #[test]
483 fn version_mismatch_is_checked_before_capabilities() {
484 let ours = Hello::v0(16384, 1024);
485 let theirs = hello(&[99], &[], &[42], 16384);
486 assert_eq!(
487 negotiate(&ours, &theirs),
488 Err(NegotiateError::NoCommonVersion)
489 );
490 }
491
492 #[test]
493 fn negotiation_is_symmetric_for_the_agreed_version() {
494 let a = hello(&[0, 1], &[], &[], 1000);
495 let b = hello(&[1, 2], &[], &[], 2000);
496 let ab = negotiate(&a, &b).unwrap();
497 let ba = negotiate(&b, &a).unwrap();
498 assert_eq!(ab.version, ba.version);
499 assert_eq!(ab.send_max_header_bytes, 2000);
500 assert_eq!(ba.send_max_header_bytes, 1000);
501 }
502
503 #[test]
504 fn errors_become_negotiation_errors() {
505 let e: weida_core::Error = NegotiateError::NoCommonVersion.into();
506 assert!(matches!(e, weida_core::Error::Negotiation(_)));
507 }
508}