1use std::{
18 sync::{
19 Arc,
20 atomic::{AtomicU64, Ordering},
21 },
22 time::{Duration, Instant},
23};
24
25use bytes::Bytes;
26use scion_quic::h3::client::{H3ResponseBody, Http3Client, RequestError as H3RequestError};
27
28use crate::{
29 config::Config,
30 epoch::Epoch,
31 error::{Error, TimeoutPhase},
32 origin::Origin,
33 request::{IntoUrl, Request},
34 response::Response,
35};
36
37enum EpochSlot {
39 Init,
42 Active(Arc<Epoch>, u64),
46 Closed,
48}
49
50pub struct Client {
70 epoch: tokio::sync::RwLock<EpochSlot>,
72 reset_generation: AtomicU64,
78 config: Config,
80 #[cfg(test)]
83 epoch_factory: Option<EpochFactory>,
84}
85
86#[cfg(test)]
87type EpochFactory =
88 Box<dyn Fn() -> futures::future::BoxFuture<'static, Result<Epoch, Error>> + Send + Sync>;
89
90impl Client {
91 #[must_use]
94 pub fn new(config: Config) -> Self {
95 Client {
96 epoch: tokio::sync::RwLock::new(EpochSlot::Init),
97 reset_generation: AtomicU64::new(0),
98 config,
99 #[cfg(test)]
100 epoch_factory: None,
101 }
102 }
103
104 #[cfg(test)]
107 pub(crate) fn with_epoch_factory(config: Config, factory: EpochFactory) -> Self {
108 let mut client = Client::new(config);
109 client.epoch_factory = Some(factory);
110 client
111 }
112
113 pub async fn request(&self, request: Request) -> Result<Response, Error> {
128 let timeout = request
129 .request_timeout()
130 .unwrap_or(self.config.request_timeout);
131 let now = tokio::time::Instant::now();
132 let deadline = now
135 .checked_add(timeout)
136 .unwrap_or_else(|| now + Duration::from_secs(86400 * 365 * 30));
137 let (response, connection) = tokio::time::timeout_at(deadline, self.request_head(&request))
138 .await
139 .map_err(|_| {
140 Error::Timeout {
141 phase: TimeoutPhase::Request,
142 timeout,
143 }
144 })??;
145 Ok(Response::new(response, deadline, timeout, connection))
146 }
147
148 pub async fn get(&self, url: impl IntoUrl) -> Result<Response, Error> {
151 let request = Request::get(url).build()?;
152 self.request(request).await
153 }
154
155 pub async fn post(&self, url: impl IntoUrl, body: impl Into<Bytes>) -> Result<Response, Error> {
159 let request = Request::post(url).body(body).build()?;
160 self.request(request).await
161 }
162
163 pub async fn warm_up(&self, url: impl IntoUrl) -> Result<(), Error> {
168 let request = Request::get(url).build()?;
169 let origin = Origin::from_request(&request)?;
170 let epoch = self.current_epoch().await?;
171 let origin_client = epoch.origin_client(origin, Instant::now(), &self.config)?;
172 origin_client.connection().await?;
173 Ok(())
174 }
175
176 pub fn reset(&self) {
184 self.reset_generation.fetch_add(1, Ordering::Release);
185 }
186
187 pub async fn close(&self) {
191 let previous = {
192 let mut slot = self.epoch.write().await;
193 std::mem::replace(&mut *slot, EpochSlot::Closed)
194 };
195 if let EpochSlot::Active(epoch, _) = previous {
196 epoch.shutdown().await;
197 }
198 }
199
200 async fn request_head(
205 &self,
206 request: &Request,
207 ) -> Result<(http::Response<H3ResponseBody>, Arc<Http3Client>), Error> {
208 let origin = Origin::from_request(request)?;
209 let (host, port) = (origin.host.clone(), origin.port);
210 let epoch = self.current_epoch().await?;
211 let origin_client = epoch.origin_client(origin, Instant::now(), &self.config)?;
212
213 let (connection, generation) = origin_client.connection().await?;
214 match connection.request(request.to_http()?).await {
215 Ok(response) => Ok((response, connection)),
216 Err(H3RequestError::Establish(_)) => {
220 let (connection, _) = origin_client.reconnect(generation).await?;
221 let response = connection
222 .request(request.to_http()?)
223 .await
224 .map_err(|e| Error::from_h3_request_error(&host, port, e))?;
225 Ok((response, connection))
226 }
227 Err(e) => Err(Error::from_h3_request_error(&host, port, e)),
228 }
229 }
230
231 async fn current_epoch(&self) -> Result<Arc<Epoch>, Error> {
239 {
240 let slot = self.epoch.read().await;
241 match &*slot {
242 EpochSlot::Closed => return Err(Error::Closed),
243 EpochSlot::Active(epoch, built)
244 if *built == self.reset_generation.load(Ordering::Acquire) =>
245 {
246 return Ok(epoch.clone());
247 }
248 _ => {}
249 }
250 }
251
252 let mut slot = self.epoch.write().await;
253 match &*slot {
254 EpochSlot::Closed => Err(Error::Closed),
255 EpochSlot::Active(epoch, built)
256 if *built == self.reset_generation.load(Ordering::Acquire) =>
257 {
258 Ok(epoch.clone())
259 }
260 _ => {
261 let generation = self.reset_generation.load(Ordering::Acquire);
266 let fresh = Arc::new(self.build_epoch().await?);
267 let previous =
268 std::mem::replace(&mut *slot, EpochSlot::Active(fresh.clone(), generation));
269 if let EpochSlot::Active(previous, _) = previous {
270 tracing::debug!("Replacing SCION connectivity epoch");
273 tokio::spawn(previous.shutdown());
274 }
275 Ok(fresh)
276 }
277 }
278 }
279
280 async fn build_epoch(&self) -> Result<Epoch, Error> {
281 #[cfg(test)]
282 if let Some(factory) = &self.epoch_factory {
283 return factory().await;
284 }
285 Epoch::build(&self.config).await
286 }
287}
288
289#[cfg(test)]
290mod tests {
291 use std::{
292 sync::atomic::{AtomicUsize, Ordering::SeqCst},
293 time::Duration,
294 };
295
296 use scion_quic::quic::config::QuicConfig;
297 use test_log::test;
298
299 use super::*;
300 use crate::test_support::{
301 FailingResolver, SERVER_PORT, StaticResolver, TestServerHarness, server_scion_ip,
302 test_config, test_router,
303 };
304
305 fn harness_client(
308 harness: Arc<TestServerHarness>,
309 resolver: Arc<dyn scion_stack::resolver::ScionDnsResolver>,
310 config: Config,
311 ) -> (Client, Arc<AtomicUsize>) {
312 let builds = Arc::new(AtomicUsize::new(0));
313 let factory_builds = builds.clone();
314 let factory_config = config.clone();
315 let client = Client::with_epoch_factory(
316 config,
317 Box::new(move || {
318 factory_builds.fetch_add(1, SeqCst);
319 let harness = harness.clone();
320 let resolver = resolver.clone();
321 let config = factory_config.clone();
322 Box::pin(async move { Ok(Epoch::new(harness, resolver, &config)) })
323 }),
324 );
325 (client, builds)
326 }
327
328 fn url(host: &str, path: &str) -> String {
329 format!("https://{host}:{SERVER_PORT}{path}")
330 }
331
332 #[test(tokio::test)]
333 #[ntest::timeout(10_000)]
334 async fn get_resolves_via_dns_path() {
335 let (router, hits) = test_router();
336 let harness = TestServerHarness::new(router);
337 let resolver = StaticResolver::new(vec![server_scion_ip()]);
338 let (client, _) = harness_client(harness, resolver, test_config());
339
340 let response = client.get(url("localhost", "/hello")).await.unwrap();
341 assert!(response.is_success());
342 let (body, _) = response.text(Some(1024)).await.unwrap();
343 assert_eq!(body, "world");
344 assert_eq!(hits.load(SeqCst), 1);
345 }
346
347 #[test(tokio::test)]
348 #[ntest::timeout(10_000)]
349 async fn duration_max_timeout_does_not_panic() {
350 let (router, _) = test_router();
351 let harness = TestServerHarness::new(router);
352 let resolver = StaticResolver::new(vec![server_scion_ip()]);
353 let (client, _) = harness_client(harness, resolver, test_config());
354
355 let request = Request::get(url("localhost", "/hello"))
356 .request_timeout(Duration::MAX)
357 .build()
358 .unwrap();
359 let response = client.request(request).await.unwrap();
360 assert!(response.is_success());
361 }
362
363 #[test(tokio::test)]
364 #[ntest::timeout(10_000)]
365 async fn target_bypasses_the_resolver() {
366 let (router, _) = test_router();
367 let harness = TestServerHarness::new(router);
368 let (client, _) = harness_client(harness, Arc::new(FailingResolver), test_config());
369
370 let request = Request::post(url("localhost", "/echo"))
371 .body("ping")
372 .target(server_scion_ip())
373 .build()
374 .unwrap();
375 let response = client.request(request).await.unwrap();
376 let (body, _) = response.text(Some(1024)).await.unwrap();
377 assert_eq!(body, "ping");
378 }
379
380 #[test(tokio::test)]
381 #[ntest::timeout(10_000)]
382 async fn resolution_failure_surfaces_as_retryable_resolution_error() {
383 let (router, _) = test_router();
384 let harness = TestServerHarness::new(router);
385 let (client, _) = harness_client(harness, Arc::new(FailingResolver), test_config());
386
387 let err = client.get(url("localhost", "/hello")).await.err().unwrap();
388 assert!(matches!(err, Error::Resolution { .. }), "{err}");
389 assert!(err.is_retryable());
390 }
391
392 #[test(tokio::test)]
393 #[ntest::timeout(10_000)]
394 async fn establish_failure_reconnects_once_and_retries() {
395 let (router, hits) = test_router();
396 let harness = TestServerHarness::new(router);
397 let resolver = StaticResolver::new(vec![server_scion_ip()]);
398 let (client, _) = harness_client(harness.clone(), resolver, test_config());
399
400 let response = client.get(url("localhost", "/hello")).await.unwrap();
401 assert!(response.is_success());
402 assert_eq!(harness.binds.load(SeqCst), 1);
403
404 harness.break_sockets();
409 tokio::time::sleep(Duration::from_millis(200)).await;
412
413 let response = client.get(url("localhost", "/hello")).await.unwrap();
414 assert!(response.is_success());
415 assert_eq!(harness.binds.load(SeqCst), 2);
416 assert_eq!(hits.load(SeqCst), 2);
417 }
418
419 #[test(tokio::test)]
420 #[ntest::timeout(10_000)]
421 async fn stream_level_errors_are_not_retried() {
422 let (router, _) = test_router();
423 let mut server_quic = QuicConfig::builder().verify_peer(false).build();
426 server_quic.initial_max_streams_bidi = 0;
427 let harness = TestServerHarness::with_server_quic(router, server_quic);
428 let resolver = StaticResolver::new(vec![server_scion_ip()]);
429 let (client, _) = harness_client(harness.clone(), resolver, test_config());
430
431 let err = client.get(url("localhost", "/hello")).await.err().unwrap();
432 assert!(matches!(err, Error::ConnectionLimit), "{err}");
433 assert_eq!(harness.binds.load(SeqCst), 1);
436 }
437
438 #[test(tokio::test)]
439 #[ntest::timeout(10_000)]
440 async fn reset_is_lazy_and_rebuild_is_single_flight() {
441 let (router, _) = test_router();
442 let harness = TestServerHarness::new(router);
443 let resolver = StaticResolver::new(vec![server_scion_ip()]);
444 let (client, builds) = harness_client(harness, resolver, test_config());
445 let client = Arc::new(client);
446
447 client.get(url("localhost", "/hello")).await.unwrap();
448 assert_eq!(builds.load(SeqCst), 1);
449
450 client.reset();
452 assert_eq!(builds.load(SeqCst), 1);
453
454 let tasks: Vec<_> = (0..3)
457 .map(|_| {
458 let client = client.clone();
459 tokio::spawn(async move { client.get(url("localhost", "/hello")).await })
460 })
461 .collect();
462 for task in tasks {
463 task.await.unwrap().unwrap();
464 }
465 assert_eq!(builds.load(SeqCst), 2);
466 }
467
468 #[test(tokio::test)]
469 #[ntest::timeout(10_000)]
470 async fn reset_during_rebuild_triggers_another_rebuild() {
471 let (router, _) = test_router();
472 let harness = TestServerHarness::new(router);
473 let resolver = StaticResolver::new(vec![server_scion_ip()]);
474 let builds = Arc::new(AtomicUsize::new(0));
475 let gate = Arc::new(tokio::sync::Semaphore::new(1));
478 let factory_builds = builds.clone();
479 let factory_gate = gate.clone();
480 let config = test_config();
481 let factory_config = config.clone();
482 let client = Arc::new(Client::with_epoch_factory(
483 config,
484 Box::new(move || {
485 let builds = factory_builds.clone();
486 let gate = factory_gate.clone();
487 let harness = harness.clone();
488 let resolver = resolver.clone();
489 let config = factory_config.clone();
490 Box::pin(async move {
491 gate.acquire().await.expect("gate closed").forget();
492 builds.fetch_add(1, SeqCst);
493 Ok(Epoch::new(harness, resolver, &config))
494 })
495 }),
496 ));
497
498 client.get(url("localhost", "/hello")).await.unwrap();
499 assert_eq!(builds.load(SeqCst), 1);
500
501 client.reset();
503 let pending = {
504 let client = client.clone();
505 tokio::spawn(async move { client.get(url("localhost", "/hello")).await })
506 };
507 tokio::time::sleep(Duration::from_millis(100)).await;
508 assert_eq!(builds.load(SeqCst), 1);
509
510 client.reset();
513 gate.add_permits(2);
514 pending.await.unwrap().unwrap();
515 assert_eq!(builds.load(SeqCst), 2);
516
517 client.get(url("localhost", "/hello")).await.unwrap();
521 assert_eq!(builds.load(SeqCst), 3);
522 }
523
524 #[test(tokio::test)]
525 #[ntest::timeout(10_000)]
526 async fn failed_rebuild_keeps_stale_and_retries_next_request() {
527 let (router, _) = test_router();
528 let harness = TestServerHarness::new(router);
529 let resolver = StaticResolver::new(vec![server_scion_ip()]);
530 let builds = Arc::new(AtomicUsize::new(0));
531 let factory_builds = builds.clone();
532 let config = test_config();
533 let factory_config = config.clone();
534 let client = Client::with_epoch_factory(
535 config,
536 Box::new(move || {
537 let build = factory_builds.fetch_add(1, SeqCst) + 1;
538 let harness = harness.clone();
539 let resolver = resolver.clone();
540 let config = factory_config.clone();
541 Box::pin(async move {
542 if build == 2 {
543 return Err(Error::StackBuild {
544 retryable: true,
545 source: "flaky discovery".into(),
546 });
547 }
548 Ok(Epoch::new(harness, resolver, &config))
549 })
550 }),
551 );
552
553 client.get(url("localhost", "/hello")).await.unwrap();
554 client.reset();
555
556 let err = client.get(url("localhost", "/hello")).await.err().unwrap();
558 assert!(matches!(err, Error::StackBuild { .. }), "{err}");
559 assert!(err.is_retryable());
560
561 client.get(url("localhost", "/hello")).await.unwrap();
563 assert_eq!(builds.load(SeqCst), 3);
564 }
565
566 #[test(tokio::test)]
567 #[ntest::timeout(10_000)]
568 async fn epochs_do_not_share_stacks_across_reset() {
569 let (router_one, _) = test_router();
571 let (router_two, _) = test_router();
572 let first = TestServerHarness::new(router_one);
573 let second = TestServerHarness::new(router_two);
574 let resolver = StaticResolver::new(vec![server_scion_ip()]);
575 let builds = Arc::new(AtomicUsize::new(0));
576 let factory_builds = builds.clone();
577 let config = test_config();
578 let factory_config = config.clone();
579 let factory_first = first.clone();
580 let factory_second = second.clone();
581 let client = Client::with_epoch_factory(
582 config,
583 Box::new(move || {
584 let build = factory_builds.fetch_add(1, SeqCst) + 1;
585 let harness = if build == 1 {
586 factory_first.clone()
587 } else {
588 factory_second.clone()
589 };
590 let resolver = resolver.clone();
591 let config = factory_config.clone();
592 Box::pin(async move { Ok(Epoch::new(harness, resolver, &config)) })
593 }),
594 );
595
596 client.get(url("localhost", "/hello")).await.unwrap();
597 assert_eq!(
598 (first.binds.load(SeqCst), second.binds.load(SeqCst)),
599 (1, 0)
600 );
601
602 client.reset();
603 client.get(url("localhost", "/hello")).await.unwrap();
604 assert_eq!(
605 (first.binds.load(SeqCst), second.binds.load(SeqCst)),
606 (1, 1)
607 );
608 }
609
610 #[test(tokio::test)]
611 #[ntest::timeout(10_000)]
612 async fn stalled_establishment_does_not_block_other_origins() {
613 let (router, _) = test_router();
616 let harness = TestServerHarness::new(router);
617 harness.stall_first_bind();
618 let resolver = StaticResolver::new(vec![server_scion_ip()]);
619 let (client, _) = harness_client(harness.clone(), resolver, test_config());
620 let client = Arc::new(client);
621
622 let stalled = {
623 let client = client.clone();
624 tokio::spawn(async move { client.get(url("a.local", "/hello")).await })
625 };
626 tokio::time::sleep(Duration::from_millis(100)).await;
627 assert!(!stalled.is_finished());
628
629 let response = client.get(url("b.local", "/hello")).await.unwrap();
632 assert!(response.is_success());
633 assert!(!stalled.is_finished());
634
635 harness.release_stalled();
636 stalled.await.unwrap().unwrap();
637 }
638
639 #[test(tokio::test)]
640 #[ntest::timeout(10_000)]
641 async fn evicted_origin_completes_in_flight_request() {
642 let (router, _) = test_router();
645 let harness = TestServerHarness::new(router);
646 let resolver = StaticResolver::new(vec![server_scion_ip()]);
647 let (client, _) = harness_client(harness, resolver, test_config().with_max_origins(1));
648 let client = Arc::new(client);
649
650 let slow = {
651 let client = client.clone();
652 tokio::spawn(async move {
653 let response = client.get(url("a.local", "/slow")).await?;
654 response.text(Some(1024)).await
655 })
656 };
657 tokio::time::sleep(Duration::from_millis(150)).await;
658
659 client.get(url("b.local", "/hello")).await.unwrap();
662
663 let (body, _) = slow.await.unwrap().unwrap();
664 assert_eq!(body, "slow");
665 }
666
667 #[test(tokio::test)]
668 #[ntest::timeout(10_000)]
669 async fn close_rejects_new_requests_and_faults_in_flight() {
670 let (router, _) = test_router();
671 let harness = TestServerHarness::new(router);
672 let resolver = StaticResolver::new(vec![server_scion_ip()]);
673 let (client, _) = harness_client(harness, resolver, test_config());
674 let client = Arc::new(client);
675
676 client.get(url("localhost", "/hello")).await.unwrap();
677
678 let in_flight = {
681 let client = client.clone();
682 tokio::spawn(async move {
683 let response = client.get(url("localhost", "/slow")).await?;
684 response.text(Some(1024)).await
685 })
686 };
687 tokio::time::sleep(Duration::from_millis(100)).await;
688 client.close().await;
689
690 let start = tokio::time::Instant::now();
691 let result = in_flight.await.unwrap();
692 assert!(result.is_err());
693 assert!(start.elapsed() < Duration::from_millis(300));
694
695 let err = client.get(url("localhost", "/hello")).await.err().unwrap();
697 assert!(matches!(err, Error::Closed), "{err}");
698 client.close().await;
699 }
700
701 #[test(tokio::test)]
702 #[ntest::timeout(10_000)]
703 async fn per_request_timeout_overrides_the_default() {
704 let (router, _) = test_router();
705 let harness = TestServerHarness::new(router);
706 let resolver = StaticResolver::new(vec![server_scion_ip()]);
707 let (client, _) = harness_client(harness, resolver, test_config());
708
709 let request = Request::get(url("localhost", "/slow"))
710 .request_timeout(Duration::from_millis(100))
711 .build()
712 .unwrap();
713 let err = client.request(request).await.err().unwrap();
714 assert!(
715 matches!(
716 err,
717 Error::Timeout {
718 phase: TimeoutPhase::Request,
719 ..
720 }
721 ),
722 "{err}"
723 );
724 }
725
726 #[test(tokio::test)]
727 #[ntest::timeout(10_000)]
728 async fn body_collection_respects_max_size() {
729 let (router, _) = test_router();
730 let harness = TestServerHarness::new(router);
731 let resolver = StaticResolver::new(vec![server_scion_ip()]);
732 let (client, _) = harness_client(harness, resolver, test_config());
733
734 let response = client.get(url("localhost", "/hello")).await.unwrap();
735 let err = response.bytes(Some(2)).await.err().unwrap();
736 assert!(matches!(err, Error::BodyTooLarge { limit: 2 }), "{err}");
737 }
738
739 #[test(tokio::test)]
740 #[ntest::timeout(10_000)]
741 async fn warm_up_establishes_without_a_request() {
742 let (router, hits) = test_router();
743 let harness = TestServerHarness::new(router);
744 let resolver = StaticResolver::new(vec![server_scion_ip()]);
745 let (client, builds) = harness_client(harness.clone(), resolver, test_config());
746
747 client.warm_up(url("localhost", "/")).await.unwrap();
748 assert_eq!(builds.load(SeqCst), 1);
749 assert_eq!(harness.binds.load(SeqCst), 1);
750 assert_eq!(hits.load(SeqCst), 0);
751
752 client.get(url("localhost", "/hello")).await.unwrap();
754 assert_eq!(harness.binds.load(SeqCst), 1);
755 assert_eq!(hits.load(SeqCst), 1);
756 }
757}