1use crate::anyhow_assert_eq;
27use crate::client::caching::cache_radius_tracker::CacheRadiusTracker;
28use crate::client::caching::post_bundle_cache_uploader;
29use crate::client::client_storage::client_storage::{ClientStorage, BUCKET_POST_BUNDLE};
30use crate::client::peer_tracker::peer_tracker::{PeerTracker, SharedPeerTracker};
31use crate::client::post_bundle::post_bundle_healing;
32use crate::client::post_bundle::post_bundle_manager::PostBundleManager;
33use crate::protocol::payload::payload::{CacheRequestTokenV1, GetPostBundleResponseV1, GetPostBundleV1, PayloadRequestKind, PayloadResponseKind};
34use crate::protocol::peer::Peer;
35use crate::protocol::posting::encoded_post_bundle::EncodedPostBundleV1;
36use crate::protocol::rpc;
37use crate::tools::buckets::BucketLocation;
38use crate::tools::config::CLIENT_POST_BUNDLE_CACHE_DURATION;
39use crate::tools::runtime_services::RuntimeServices;
40use crate::tools::time::TimeMillis;
41use crate::tools::tools::{spawn_background_task, LeadingAgreementBits};
42use crate::tools::types::Id;
43use crate::tools::{config, json};
44use bytes::Bytes;
45use log::{info, trace, warn};
46use std::collections::HashMap;
47use std::sync::Arc;
48use tokio::sync::watch;
49
50pub struct LivePostBundleManager {
69 inner: Arc<LivePostBundleManagerInner>,
70}
71
72struct LivePostBundleManagerInner {
74 runtime_services: Arc<RuntimeServices>,
75 client_storage: Arc<dyn ClientStorage>,
76 peer_tracker: SharedPeerTracker,
77 sponsor_id: Id,
78 inflight_post_bundle_fetches: InflightPostBundleFetches,
79 post_bundle_cache_radius_tracker: CacheRadiusTracker,
80}
81
82impl LivePostBundleManager {
83 pub fn new(
84 runtime_services: Arc<RuntimeServices>,
85 sponsor_id: Id,
86 client_storage: Arc<dyn ClientStorage>,
87 peer_tracker: SharedPeerTracker,
88 ) -> Self {
89 Self {
90 inner: Arc::new(LivePostBundleManagerInner {
91 runtime_services,
92 client_storage,
93 peer_tracker,
94 sponsor_id,
95 inflight_post_bundle_fetches: InflightPostBundleFetches::default(),
96 post_bundle_cache_radius_tracker: CacheRadiusTracker::new(CLIENT_POST_BUNDLE_CACHE_DURATION.const_mul(5)), }),
98 }
99 }
100}
101
102#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
103#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
104impl PostBundleManager
105 for LivePostBundleManager
106{
107 async fn get_post_bundle(&self, bucket_location: &BucketLocation, time_millis: TimeMillis) -> anyhow::Result<EncodedPostBundleV1> {
108 self.inner.fetch(bucket_location, time_millis, false).await
109 }
110
111 async fn get_post_bundle_settled(&self, bucket_location: &BucketLocation, time_millis: TimeMillis) -> anyhow::Result<EncodedPostBundleV1> {
112 self.inner.fetch(bucket_location, time_millis, true).await
113 }
114}
115
116impl LivePostBundleManagerInner {
117 async fn get_from_cache(&self, location_id: Id, time_millis: TimeMillis, reason: &str) -> Option<EncodedPostBundleV1> {
118 let result: anyhow::Result<Option<EncodedPostBundleV1>> = try {
119 let raw = self.client_storage.get(BUCKET_POST_BUNDLE, &location_id.to_hex_str(), time_millis).await?;
120 if let Some(raw) = raw {
121 let bundle = EncodedPostBundleV1::from_bytes(Bytes::from(raw), true)?;
124 if bundle.header.sealed {
125 trace!("Using cached sealed PostBundle for {} at {}", location_id, reason);
126 Some(bundle)
127 }
128 else {
129 let duration = time_millis - bundle.header.time_millis;
130 if duration < CLIENT_POST_BUNDLE_CACHE_DURATION {
131 trace!("Using cached PostBundle for {} (age {}) at {}", location_id, duration, reason);
132 Some(bundle)
133 }
134 else {
135 trace!("Cached PostBundle for {} expired (age {}) at {}", location_id, duration, reason);
136 None
137 }
138 }
139 }
140 else {
141 None
142 }
143 };
144
145 result.unwrap_or_else(|e| {
146 warn!("discarding problematic cached PostBundle: {}", e);
147 None
148 })
149 }
150
151 async fn fetch(self: &Arc<Self>, bucket_location: &BucketLocation, time_millis: TimeMillis, wait_for_settled: bool) -> anyhow::Result<EncodedPostBundleV1> {
153 let location_id = bucket_location.location_id;
154
155 if let Some(cached) = self.get_from_cache(location_id, time_millis, "preflight").await {
157 return Ok(cached);
158 }
159
160 let (mut fetch_state_receiver, fetch_state_sender) = self.inflight_post_bundle_fetches.join_or_start(location_id);
161 if let Some(fetch_state_sender) = fetch_state_sender {
162 if let Some(cached) = self.get_from_cache(location_id, time_millis, "postflight").await {
164 fetch_state_sender.send_replace(PostBundleFetchState::Settled(Ok(cached)));
165 self.inflight_post_bundle_fetches.finish(location_id);
166 }
167 else {
168 spawn_background_task(self.clone().walk_and_settle(bucket_location.clone(), time_millis, fetch_state_sender));
169 }
170 }
171
172 await_fetch_answer(location_id, &mut fetch_state_receiver, wait_for_settled).await
173 }
174
175 async fn walk_and_settle(self: Arc<Self>, bucket_location: BucketLocation, time_millis: TimeMillis, fetch_state_sender: watch::Sender<PostBundleFetchState>) {
178 let location_id = bucket_location.location_id;
179
180 match self.walk(&bucket_location, time_millis, &fetch_state_sender).await {
181 Ok((best_encoded_post_bundle, accepted_post_bundles, cache_request_tokens)) => {
182 fetch_state_sender.send_replace(PostBundleFetchState::Settled(Ok(best_encoded_post_bundle)));
183 self.inflight_post_bundle_fetches.finish(location_id);
184
185 post_bundle_healing::heal_post_bundles(self.runtime_services.clone(), self.sponsor_id, bucket_location.clone(), &accepted_post_bundles.donor_bundles(), accepted_post_bundles.direct_bundles());
187
188 post_bundle_cache_uploader::upload_post_bundle_caches(self.runtime_services.clone(), self.sponsor_id, cache_request_tokens, accepted_post_bundles.bundle_bytes_for_upload());
190 }
191 Err(error) => {
192 warn!("PostBundle walk for {} failed: {:#}", location_id, error);
193 fetch_state_sender.send_replace(PostBundleFetchState::Settled(Err(Arc::new(error))));
194 self.inflight_post_bundle_fetches.finish(location_id);
195 }
196 }
197 }
198
199 async fn walk(&self, bucket_location: &BucketLocation, time_millis: TimeMillis, fetch_state_sender: &watch::Sender<PostBundleFetchState>) -> anyhow::Result<(EncodedPostBundleV1, AcceptedPostBundles, Vec<CacheRequestTokenV1>)> {
201 let cache_radius = self.post_bundle_cache_radius_tracker.get(bucket_location.location_id, time_millis);
202
203 let mut peers_visited = Vec::new();
205 let mut accepted_post_bundles = AcceptedPostBundles::new(bucket_location.clone(), time_millis);
206 let mut cache_request_tokens: Vec<CacheRequestTokenV1> = Vec::new();
207 let mut positive_responder_leading_agreement_bits: Vec<LeadingAgreementBits> = Vec::new();
208 let mut provisional_published = false;
209 {
210 let mut peer_iter = PeerTracker::iterate_to_location(&self.peer_tracker, bucket_location.location_id, 2 * config::REDUNDANT_SERVERS_PER_POST, cache_radius).await?;
211 while let Some((peer, leading_agreement_bits)) = peer_iter.next_peer() {
212 if accepted_post_bundles.accepted_originator_ids().contains(&peer.id) {
213 continue;
214 }
215
216 let result: anyhow::Result<()> = try {
217 info!("Requesting PostBundle bucket_location={} with leading_agreement_bits={} from peer {}", bucket_location, leading_agreement_bits, peer);
218
219 let request = GetPostBundleV1 {
220 bucket_location: bucket_location.clone(),
221 peers_visited: peers_visited.clone(),
222 already_retrieved_peer_ids: accepted_post_bundles.accepted_originator_ids().to_vec(),
223 };
224 let request = json::struct_to_bytes(&request)?;
225 let response = rpc::rpc::rpc_server_known(&self.runtime_services, &self.sponsor_id, &peer, PayloadRequestKind::GetPostBundleV1, request).await?;
226 anyhow_assert_eq!(&PayloadResponseKind::GetPostBundleResponseV1, &response.response_request_kind);
227 let response = GetPostBundleResponseV1::from_bytes(response.bytes)?;
228
229 peers_visited.push(peer.clone());
230 peer_iter.add_peers(response.peers_nearer);
231
232 if let Some(token) = response.cache_request_token {
233 cache_request_tokens.push(token);
234 }
235
236 let mut found_bundle_from_this_peer = false;
237
238 for cached_bytes in response.post_bundles_cached {
239 match accepted_post_bundles.offer_cached(cached_bytes) {
240 Ok(accepted) => found_bundle_from_this_peer |= accepted,
241 Err(e) => warn!("Rejecting cached PostBundle from peer {}: {}", peer, e),
242 }
243 }
244
245 if let Some(post_bundle_raw) = response.post_bundle {
247 found_bundle_from_this_peer |= accepted_post_bundles.offer_direct(post_bundle_raw, &peer)?;
248 }
249
250 if found_bundle_from_this_peer {
251 positive_responder_leading_agreement_bits.push(leading_agreement_bits);
252 }
253
254 if !provisional_published && accepted_post_bundles.has_authoritative() {
256 if let Some(provisional_encoded_post_bundle) = accepted_post_bundles.select_best() {
257 trace!("Publishing provisional PostBundle for {} with {} posts", bucket_location.location_id, provisional_encoded_post_bundle.header.num_posts);
258 fetch_state_sender.send_replace(PostBundleFetchState::Provisional(provisional_encoded_post_bundle));
259 provisional_published = true;
260 }
261 }
262
263 if accepted_post_bundles.quorum_count() >= config::REDUNDANT_SERVERS_PER_POST {
264 break;
265 }
266 };
267
268 if let Err(e) = result {
269 warn!("Error retrieving PostBundle from peer {}: {}", peer, e);
270 peer_iter.remove_peer(&peer);
271 }
272 }
273
274 let result = PeerTracker::flush(&self.peer_tracker).await;
275 if let Err(e) = result {
276 warn!("Error flushing peer tracker: {}", e);
277 }
278 }
279
280 info!("Accepted post bundles from {} originators after visiting {} peers", accepted_post_bundles.quorum_count(), peers_visited.len());
281
282 if let Some(new_radius) = positive_responder_leading_agreement_bits.iter().copied().min() {
284 self.post_bundle_cache_radius_tracker.update(bucket_location.location_id, new_radius, time_millis);
285 }
286
287 let best_encoded_post_bundle = accepted_post_bundles.select_best().ok_or_else(|| anyhow::anyhow!("No post bundles discovered for {}", bucket_location.location_id))?;
288
289 trace!("Caching PostBundle for {} with {} posts", bucket_location.location_id, best_encoded_post_bundle.header.num_posts);
291 let encoded_post_bundle_bytes = best_encoded_post_bundle.to_bytes()?;
292 self.client_storage.put(BUCKET_POST_BUNDLE, &bucket_location.location_id.to_hex_str(), encoded_post_bundle_bytes.to_vec(), time_millis).await?;
293
294 Ok((best_encoded_post_bundle, accepted_post_bundles, cache_request_tokens))
295 }
296}
297
298async fn await_fetch_answer(location_id: Id, fetch_state_receiver: &mut watch::Receiver<PostBundleFetchState>, wait_for_settled: bool) -> anyhow::Result<EncodedPostBundleV1> {
300 let fetch_state = fetch_state_receiver
301 .wait_for(|fetch_state| match fetch_state {
302 PostBundleFetchState::Walking => false,
303 PostBundleFetchState::Provisional(_) => !wait_for_settled,
304 PostBundleFetchState::Settled(_) => true,
305 })
306 .await
307 .map_err(|_| anyhow::anyhow!("PostBundle walk for {} ended without settling", location_id))?
308 .clone();
309
310 match fetch_state {
311 PostBundleFetchState::Provisional(encoded_post_bundle) => Ok(encoded_post_bundle),
312 PostBundleFetchState::Settled(Ok(encoded_post_bundle)) => Ok(encoded_post_bundle),
313 PostBundleFetchState::Settled(Err(error)) => Err(anyhow::Error::new(PostBundleFetchError { location_id, error })),
314 PostBundleFetchState::Walking => anyhow::bail!("PostBundle walk for {} is still walking", location_id),
315 }
316}
317
318#[derive(Clone)]
320enum PostBundleFetchState {
321 Walking,
322 Provisional(EncodedPostBundleV1),
324 Settled(Result<EncodedPostBundleV1, Arc<anyhow::Error>>),
326}
327
328#[derive(Debug)]
331struct PostBundleFetchError {
332 location_id: Id,
333 error: Arc<anyhow::Error>,
334}
335
336impl std::fmt::Display for PostBundleFetchError {
337 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
338 write!(formatter, "fetching PostBundle {} failed", self.location_id)
339 }
340}
341
342impl std::error::Error for PostBundleFetchError {
343 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
344 let error: &(dyn std::error::Error + Send + Sync + 'static) = self.error.as_ref().as_ref();
345 Some(error)
346 }
347}
348
349#[derive(Default)]
351struct InflightPostBundleFetches {
352 fetches: parking_lot::Mutex<HashMap<Id, watch::Receiver<PostBundleFetchState>>>,
353}
354
355impl InflightPostBundleFetches {
356 fn join_or_start(&self, location_id: Id) -> (watch::Receiver<PostBundleFetchState>, Option<watch::Sender<PostBundleFetchState>>) {
358 let mut fetches = self.fetches.lock();
359 if let Some(fetch_state_receiver) = fetches.get(&location_id) {
360 if fetch_state_receiver.has_changed().is_ok() {
362 return (fetch_state_receiver.clone(), None);
363 }
364 }
365 let (fetch_state_sender, fetch_state_receiver) = watch::channel(PostBundleFetchState::Walking);
366 fetches.insert(location_id, fetch_state_receiver.clone());
367 (fetch_state_receiver, Some(fetch_state_sender))
368 }
369
370 fn finish(&self, location_id: Id) {
372 self.fetches.lock().remove(&location_id);
373 }
374}
375
376struct AcceptedPostBundles {
386 bucket_location: BucketLocation,
387 time_millis: TimeMillis,
388 accepted_originator_ids: Vec<Id>,
389 authoritative_bundles: Vec<(Bytes, EncodedPostBundleV1)>,
390 direct_bundles: Vec<(Peer, EncodedPostBundleV1)>,
391}
392
393impl AcceptedPostBundles {
394 fn new(bucket_location: BucketLocation, time_millis: TimeMillis) -> Self {
395 Self { bucket_location, time_millis, accepted_originator_ids: Vec::new(), authoritative_bundles: Vec::new(), direct_bundles: Vec::new() }
396 }
397
398 fn accepted_originator_ids(&self) -> &[Id] {
400 &self.accepted_originator_ids
401 }
402
403 fn has_authoritative(&self) -> bool {
405 !self.authoritative_bundles.is_empty()
406 }
407
408 fn quorum_count(&self) -> usize {
410 self.accepted_originator_ids.len()
411 }
412
413 fn offer_cached(&mut self, encoded_post_bundle_bytes: Bytes) -> anyhow::Result<bool> {
415 let encoded_post_bundle = EncodedPostBundleV1::from_bytes(encoded_post_bundle_bytes.clone(), true)?;
416 if !self.verify_new_originator(&encoded_post_bundle)? {
417 return Ok(false);
418 }
419 if encoded_post_bundle.header.num_posts == 0 {
420 trace!("Ignoring empty cached PostBundle from peer.id={}", encoded_post_bundle.header.peer.id);
421 return Ok(false);
422 }
423
424 trace!("Accepted cached PostBundle from peer.id={} with {} posts", encoded_post_bundle.header.peer.id, encoded_post_bundle.header.num_posts);
425 self.accepted_originator_ids.push(encoded_post_bundle.header.peer.id);
426 self.authoritative_bundles.push((encoded_post_bundle_bytes, encoded_post_bundle));
427 Ok(true)
428 }
429
430 fn offer_direct(&mut self, encoded_post_bundle_bytes: Bytes, responding_peer: &Peer) -> anyhow::Result<bool> {
432 let encoded_post_bundle = EncodedPostBundleV1::from_bytes(encoded_post_bundle_bytes.clone(), true)?;
433 if encoded_post_bundle.header.peer.id != responding_peer.id {
434 anyhow::bail!("PostBundle from peer {} claims to be published by {}", responding_peer.id, encoded_post_bundle.header.peer.id);
435 }
436 if !self.verify_new_originator(&encoded_post_bundle)? {
437 return Ok(false);
438 }
439
440 trace!("Accepted PostBundle from peer.id={} with {} posts", encoded_post_bundle.header.peer.id, encoded_post_bundle.header.num_posts);
441 self.accepted_originator_ids.push(encoded_post_bundle.header.peer.id);
442 if encoded_post_bundle.header.num_posts > 0 {
443 self.authoritative_bundles.push((encoded_post_bundle_bytes, encoded_post_bundle.clone()));
444 }
445 self.direct_bundles.push((responding_peer.clone(), encoded_post_bundle));
446 Ok(true)
447 }
448
449 fn verify_new_originator(&self, encoded_post_bundle: &EncodedPostBundleV1) -> anyhow::Result<bool> {
451 if self.accepted_originator_ids.contains(&encoded_post_bundle.header.peer.id) {
452 return Ok(false);
453 }
454 encoded_post_bundle.verify_for_bucket(&self.bucket_location, self.time_millis)?;
455 Ok(true)
456 }
457
458 fn select_best(&self) -> Option<EncodedPostBundleV1> {
464 let authoritative = self.authoritative_bundles.iter().map(|(_, encoded_post_bundle)| encoded_post_bundle);
465 let direct = self.direct_bundles.iter().map(|(_, encoded_post_bundle)| encoded_post_bundle);
466
467 let mut best_encoded_post_bundle = authoritative.clone().max_by_key(|encoded_post_bundle| encoded_post_bundle.header.num_posts).or_else(|| direct.clone().next())?.clone();
468 best_encoded_post_bundle.header.overflowed = authoritative
469 .chain(direct)
470 .any(|encoded_post_bundle| encoded_post_bundle.header.overflowed || encoded_post_bundle.header.num_posts > config::ENCODED_POST_BUNDLE_V1_OVERFLOWED_NUM_POSTS);
471 best_encoded_post_bundle.normalise_flags_for_bucket(&self.bucket_location, self.time_millis);
472 Some(best_encoded_post_bundle)
473 }
474
475 fn donor_bundles(&self) -> Vec<EncodedPostBundleV1> {
476 self.authoritative_bundles.iter().map(|(_, encoded_post_bundle)| encoded_post_bundle.clone()).collect()
477 }
478
479 fn direct_bundles(&self) -> &[(Peer, EncodedPostBundleV1)] {
480 &self.direct_bundles
481 }
482
483 fn bundle_bytes_for_upload(&self) -> Vec<(Id, Bytes)> {
484 self.authoritative_bundles.iter().map(|(encoded_post_bundle_bytes, encoded_post_bundle)| (encoded_post_bundle.header.peer.id, encoded_post_bundle_bytes.clone())).collect()
485 }
486}
487
488#[cfg(test)]
489mod tests {
490 use super::*;
491 use crate::protocol::posting::encoded_post_bundle::test_fixtures::BucketBundleFixture;
492 use crate::tools::pow_generator::single_threaded_pow_generator::SingleThreadedPowGenerator;
493 use crate::tools::server_id::ServerId;
494 use crate::tools::time_provider::time_provider::RealTimeProvider;
495 use crate::tools::types::Pow;
496
497 fn new_accepted_post_bundles(fixture: &BucketBundleFixture) -> AcceptedPostBundles {
498 AcceptedPostBundles::new(fixture.bucket_location.clone(), fixture.time_millis_now)
499 }
500
501 #[tokio::test]
502 async fn forged_or_empty_cached_bundles_do_not_count() -> anyhow::Result<()> {
503 let fixture = BucketBundleFixture::new().await?;
504 let posts = fixture.make_own_posts(1).await?;
505 let mut accepted_post_bundles = new_accepted_post_bundles(&fixture);
506
507 for _ in 0..config::REDUNDANT_SERVERS_PER_POST {
509 let server_id = BucketBundleFixture::new_server_id().await?;
510 assert!(!accepted_post_bundles.offer_cached(fixture.make_bundle_from(&server_id, &[])?.to_bytes()?)?);
511 }
512
513 let unworked_server_id = loop {
515 let server_id = ServerId::new("unworked", &RealTimeProvider, Pow(0), true, &SingleThreadedPowGenerator::new()).await?;
516 if server_id.pow < config::SERVER_KEY_POW_MIN {
517 break server_id;
518 }
519 };
520 assert!(accepted_post_bundles.offer_cached(fixture.make_bundle_from(&unworked_server_id, &posts)?.to_bytes()?).is_err());
521
522 assert_eq!(accepted_post_bundles.quorum_count(), 0);
523 assert!(accepted_post_bundles.accepted_originator_ids().is_empty());
524 assert!(accepted_post_bundles.select_best().is_none());
525 assert!(accepted_post_bundles.bundle_bytes_for_upload().is_empty());
526 Ok(())
527 }
528
529 #[tokio::test]
530 async fn direct_bundle_naming_another_peer_is_rejected() -> anyhow::Result<()> {
531 let fixture = BucketBundleFixture::new().await?;
532 let posts = fixture.make_own_posts(1).await?;
533 let mut accepted_post_bundles = new_accepted_post_bundles(&fixture);
534
535 let responding_server_id = BucketBundleFixture::new_server_id().await?;
536 let other_server_id = BucketBundleFixture::new_server_id().await?;
537 let other_bundle = fixture.make_bundle_from(&other_server_id, &posts)?;
538 assert!(accepted_post_bundles.offer_direct(other_bundle.to_bytes()?, &responding_server_id.to_peer(&RealTimeProvider)?).is_err());
539 assert_eq!(accepted_post_bundles.quorum_count(), 0);
540 Ok(())
541 }
542
543 #[tokio::test]
544 async fn empty_bucket_reaches_quorum_via_direct_empties() -> anyhow::Result<()> {
545 let fixture = BucketBundleFixture::new().await?;
546 let mut accepted_post_bundles = new_accepted_post_bundles(&fixture);
547
548 for _ in 0..config::REDUNDANT_SERVERS_PER_POST {
549 let server_id = BucketBundleFixture::new_server_id().await?;
550 assert!(accepted_post_bundles.offer_direct(fixture.make_bundle_from(&server_id, &[])?.to_bytes()?, &server_id.to_peer(&RealTimeProvider)?)?);
551 }
552
553 assert_eq!(accepted_post_bundles.quorum_count(), config::REDUNDANT_SERVERS_PER_POST);
554 let best_encoded_post_bundle = accepted_post_bundles.select_best().expect("an empty bundle");
555 assert_eq!(best_encoded_post_bundle.header.num_posts, 0);
556 assert!(accepted_post_bundles.donor_bundles().is_empty(), "empty bundles have nothing to donate");
557 assert!(accepted_post_bundles.bundle_bytes_for_upload().is_empty(), "empty bundles are not worth uploading to caches");
558 assert_eq!(accepted_post_bundles.direct_bundles().len(), config::REDUNDANT_SERVERS_PER_POST, "but they are heal targets");
559 Ok(())
560 }
561
562 #[tokio::test]
563 async fn each_originator_counts_once() -> anyhow::Result<()> {
564 let fixture = BucketBundleFixture::new().await?;
565 let posts = fixture.make_own_posts(1).await?;
566 let mut accepted_post_bundles = new_accepted_post_bundles(&fixture);
567
568 let server_id = BucketBundleFixture::new_server_id().await?;
569 let encoded_post_bundle_bytes = fixture.make_bundle_from(&server_id, &posts)?.to_bytes()?;
570 assert!(accepted_post_bundles.offer_cached(encoded_post_bundle_bytes.clone())?);
571 assert!(!accepted_post_bundles.offer_cached(encoded_post_bundle_bytes.clone())?);
572 assert!(!accepted_post_bundles.offer_direct(encoded_post_bundle_bytes, &server_id.to_peer(&RealTimeProvider)?)?);
573 assert_eq!(accepted_post_bundles.quorum_count(), 1);
574 Ok(())
575 }
576
577 #[tokio::test]
578 async fn selects_largest_bundle_with_overflowed_or_merged() -> anyhow::Result<()> {
579 let fixture = BucketBundleFixture::new().await?;
580 let posts = fixture.make_own_posts(5).await?;
581 let mut accepted_post_bundles = new_accepted_post_bundles(&fixture);
582
583 let server_id_a = BucketBundleFixture::new_server_id().await?;
585 let mut encoded_post_bundle_a = fixture.make_bundle_from(&server_id_a, &posts[..3])?;
586 encoded_post_bundle_a.header.overflowed = true;
587 encoded_post_bundle_a.header.sealed = true;
588 encoded_post_bundle_a.header.signature_generate(&server_id_a.keys.signature_key)?;
589 let encoded_post_bundle_bytes_a = encoded_post_bundle_a.to_bytes()?;
590
591 let server_id_b = BucketBundleFixture::new_server_id().await?;
593 let encoded_post_bundle_b = fixture.make_bundle_from(&server_id_b, &posts)?;
594 let encoded_post_bundle_bytes_b = encoded_post_bundle_b.to_bytes()?;
595
596 assert!(accepted_post_bundles.offer_direct(encoded_post_bundle_bytes_a.clone(), &server_id_a.to_peer(&RealTimeProvider)?)?);
597 assert!(accepted_post_bundles.offer_cached(encoded_post_bundle_bytes_b.clone())?);
598
599 let best_encoded_post_bundle = accepted_post_bundles.select_best().expect("a bundle");
600 assert_eq!(best_encoded_post_bundle.header.encoded_post_ids, encoded_post_bundle_b.header.encoded_post_ids, "posts come from the largest bundle");
601 assert!(best_encoded_post_bundle.header.overflowed, "overflowed is OR-ed across all accepted bundles");
602 assert!(best_encoded_post_bundle.header.sealed, "an overflowed bucket is sealed");
603
604 let cached_encoded_post_bundle = EncodedPostBundleV1::from_bytes(best_encoded_post_bundle.to_bytes()?, true)?;
606 assert!(cached_encoded_post_bundle.header.overflowed);
607
608 let mut bundle_bytes_for_upload = accepted_post_bundles.bundle_bytes_for_upload();
610 bundle_bytes_for_upload.sort_by_key(|(originator_id, _)| *originator_id);
611 let mut expected_bundle_bytes_for_upload = vec![(server_id_a.id, encoded_post_bundle_bytes_a), (server_id_b.id, encoded_post_bundle_bytes_b)];
612 expected_bundle_bytes_for_upload.sort_by_key(|(originator_id, _)| *originator_id);
613 assert_eq!(bundle_bytes_for_upload, expected_bundle_bytes_for_upload);
614 assert!(accepted_post_bundles.donor_bundles().iter().any(|donor_bundle| *donor_bundle == encoded_post_bundle_b));
615 Ok(())
616 }
617
618 #[tokio::test]
619 async fn premature_sealed_claim_is_dropped() -> anyhow::Result<()> {
620 let fixture = BucketBundleFixture::new().await?;
621 let posts = fixture.make_own_posts(1).await?;
622 let mut accepted_post_bundles = new_accepted_post_bundles(&fixture);
623
624 let server_id = BucketBundleFixture::new_server_id().await?;
625 let mut encoded_post_bundle = fixture.make_bundle_from(&server_id, &posts)?;
626 encoded_post_bundle.header.sealed = true; encoded_post_bundle.header.signature_generate(&server_id.keys.signature_key)?;
628 assert!(accepted_post_bundles.offer_cached(encoded_post_bundle.to_bytes()?)?);
629
630 assert!(!accepted_post_bundles.select_best().expect("a bundle").header.sealed);
631 Ok(())
632 }
633
634 #[tokio::test]
635 async fn has_authoritative_only_once_a_bundle_with_posts_is_accepted() -> anyhow::Result<()> {
636 let fixture = BucketBundleFixture::new().await?;
637 let posts = fixture.make_own_posts(1).await?;
638 let mut accepted_post_bundles = new_accepted_post_bundles(&fixture);
639 assert!(!accepted_post_bundles.has_authoritative());
640
641 let empty_server_id = BucketBundleFixture::new_server_id().await?;
643 assert!(accepted_post_bundles.offer_direct(fixture.make_bundle_from(&empty_server_id, &[])?.to_bytes()?, &empty_server_id.to_peer(&RealTimeProvider)?)?);
644 assert!(!accepted_post_bundles.has_authoritative());
645
646 let cached_empty_server_id = BucketBundleFixture::new_server_id().await?;
648 assert!(!accepted_post_bundles.offer_cached(fixture.make_bundle_from(&cached_empty_server_id, &[])?.to_bytes()?)?);
649 assert!(!accepted_post_bundles.has_authoritative());
650
651 let server_id = BucketBundleFixture::new_server_id().await?;
652 assert!(accepted_post_bundles.offer_cached(fixture.make_bundle_from(&server_id, &posts)?.to_bytes()?)?);
653 assert!(accepted_post_bundles.has_authoritative());
654 Ok(())
655 }
656
657 async fn some_encoded_post_bundle() -> anyhow::Result<EncodedPostBundleV1> {
660 let fixture = BucketBundleFixture::new().await?;
661 let posts = fixture.make_own_posts(1).await?;
662 let server_id = BucketBundleFixture::new_server_id().await?;
663 fixture.make_bundle_from(&server_id, &posts)
664 }
665
666 #[tokio::test]
667 async fn first_caller_starts_the_walk_and_later_callers_join_it() {
668 let inflight_post_bundle_fetches = InflightPostBundleFetches::default();
669 let location_id = Id::random();
670
671 let (_, starter_fetch_state_sender) = inflight_post_bundle_fetches.join_or_start(location_id);
672 assert!(starter_fetch_state_sender.is_some(), "the first caller starts the walk");
673 let (_, joiner_fetch_state_sender) = inflight_post_bundle_fetches.join_or_start(location_id);
674 assert!(joiner_fetch_state_sender.is_none(), "a second caller joins it");
675
676 let (_, other_location_fetch_state_sender) = inflight_post_bundle_fetches.join_or_start(Id::random());
677 assert!(other_location_fetch_state_sender.is_some(), "other locations get their own walk");
678 }
679
680 #[tokio::test]
681 async fn finish_lets_the_next_caller_start_afresh() {
682 let inflight_post_bundle_fetches = InflightPostBundleFetches::default();
683 let location_id = Id::random();
684
685 let (_, fetch_state_sender) = inflight_post_bundle_fetches.join_or_start(location_id);
686 let _fetch_state_sender = fetch_state_sender.expect("starter");
687 inflight_post_bundle_fetches.finish(location_id);
688 assert!(inflight_post_bundle_fetches.join_or_start(location_id).1.is_some());
689 }
690
691 #[tokio::test]
692 async fn a_walk_that_died_without_settling_does_not_block_later_callers() {
693 let inflight_post_bundle_fetches = InflightPostBundleFetches::default();
694 let location_id = Id::random();
695
696 let (mut fetch_state_receiver, fetch_state_sender) = inflight_post_bundle_fetches.join_or_start(location_id);
697 drop(fetch_state_sender); assert!(await_fetch_answer(location_id, &mut fetch_state_receiver, false).await.is_err(), "its own waiters get an error, not a hang");
699 assert!(inflight_post_bundle_fetches.join_or_start(location_id).1.is_some(), "the next caller starts a new walk");
700 }
701
702 #[tokio::test]
703 async fn joiners_see_a_provisional_bundle_immediately() -> anyhow::Result<()> {
704 let inflight_post_bundle_fetches = InflightPostBundleFetches::default();
705 let location_id = Id::random();
706 let provisional_encoded_post_bundle = some_encoded_post_bundle().await?;
707
708 let (_, fetch_state_sender) = inflight_post_bundle_fetches.join_or_start(location_id);
709 let fetch_state_sender = fetch_state_sender.expect("starter");
710 fetch_state_sender.send_replace(PostBundleFetchState::Provisional(provisional_encoded_post_bundle.clone()));
711
712 let (mut joiner_fetch_state_receiver, _) = inflight_post_bundle_fetches.join_or_start(location_id);
713 assert_eq!(provisional_encoded_post_bundle, await_fetch_answer(location_id, &mut joiner_fetch_state_receiver, false).await?);
714 Ok(())
715 }
716
717 #[tokio::test]
718 async fn settled_waiters_ignore_the_provisional_bundle() -> anyhow::Result<()> {
719 let location_id = Id::random();
720 let provisional_encoded_post_bundle = some_encoded_post_bundle().await?;
721 let settled_encoded_post_bundle = some_encoded_post_bundle().await?;
722 let (fetch_state_sender, fetch_state_receiver) = watch::channel(PostBundleFetchState::Walking);
723
724 let mut early_fetch_state_receiver = fetch_state_receiver.clone();
725 let mut settled_fetch_state_receiver = fetch_state_receiver.clone();
726 let early_answer = tokio::spawn(async move { await_fetch_answer(location_id, &mut early_fetch_state_receiver, false).await });
727 let settled_answer = tokio::spawn(async move { await_fetch_answer(location_id, &mut settled_fetch_state_receiver, true).await });
728
729 fetch_state_sender.send_replace(PostBundleFetchState::Provisional(provisional_encoded_post_bundle.clone()));
730 assert_eq!(provisional_encoded_post_bundle, early_answer.await??);
731 tokio::task::yield_now().await;
732 assert!(!settled_answer.is_finished(), "a settled waiter must not wake on the provisional bundle");
733
734 fetch_state_sender.send_replace(PostBundleFetchState::Settled(Ok(settled_encoded_post_bundle.clone())));
735 assert_eq!(settled_encoded_post_bundle, settled_answer.await??);
736 Ok(())
737 }
738
739 #[derive(Debug)]
740 struct WalkRootCause;
741 impl std::fmt::Display for WalkRootCause {
742 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
743 write!(formatter, "the root cause")
744 }
745 }
746 impl std::error::Error for WalkRootCause {}
747
748 #[tokio::test]
749 async fn walk_errors_reach_every_waiter_with_their_chain_intact() -> anyhow::Result<()> {
750 let location_id = Id::random();
751 let (fetch_state_sender, fetch_state_receiver) = watch::channel(PostBundleFetchState::Walking);
752 let walk_error = anyhow::Error::new(WalkRootCause).context("walking towards the bucket");
753 fetch_state_sender.send_replace(PostBundleFetchState::Settled(Err(Arc::new(walk_error))));
754
755 for wait_for_settled in [false, true] {
756 let mut waiter_fetch_state_receiver = fetch_state_receiver.clone();
757 let error = await_fetch_answer(location_id, &mut waiter_fetch_state_receiver, wait_for_settled).await.expect_err("the walk failed");
758
759 let formatted_error = format!("{:#}", error);
760 assert!(formatted_error.contains(&location_id.to_string()), "names the location: {}", formatted_error);
761 assert!(formatted_error.contains("walking towards the bucket"), "keeps the context: {}", formatted_error);
762 assert!(formatted_error.contains("the root cause"), "keeps the root cause: {}", formatted_error);
763 assert!(error.chain().any(|cause| cause.downcast_ref::<WalkRootCause>().is_some()), "the original error stays reachable");
764 assert!(error.downcast_ref::<PostBundleFetchError>().is_some());
765 }
766 Ok(())
767 }
768}