diff --git a/crates/fetch/src/fake.rs b/crates/fetch/src/fake.rs index 2669a4c13..3ac605f03 100644 --- a/crates/fetch/src/fake.rs +++ b/crates/fetch/src/fake.rs @@ -172,7 +172,10 @@ mod tests { #[cfg_attr(miri, ignore)] #[test] fn fake_builder_custom_clock() { - let clock = tick::ClockControl::new().auto_advance(Duration::from_secs(2)).to_clock(); + let clock = tick::ClockControl::builder() + .auto_advance(Duration::from_secs(2)) + .build() + .to_clock(); let _client = HttpClient::builder_fake(FakeHandler::never_completes(), &clock) .custom_pipeline(|root, ctx| { diff --git a/crates/fetch/tests/resilience.rs b/crates/fetch/tests/resilience.rs index be07a412a..9f638c167 100644 --- a/crates/fetch/tests/resilience.rs +++ b/crates/fetch/tests/resilience.rs @@ -65,7 +65,7 @@ async fn retry_defaults_restore_requests() { let index = req.extensions().get::().copied().unwrap().index(); HttpError::unavailable(format!("unavailable-{index}")).with_request(req) }); - let clock = ClockControl::default().auto_advance_timers(true).to_clock(); + let clock = ClockControl::new_auto_advancing().to_clock(); let client = HttpClient::builder_fake(handler, FakeDeps { clock }) .custom_pipeline(move |dispatch, context| { let layer = HttpRetry::layer("dummy", context.resilience_context()) @@ -101,7 +101,7 @@ async fn retry_defaults_non_cloneable_body() { } fn create_retry_client(status: StatusCode) -> HttpClient { - let clock = ClockControl::default().auto_advance_timers(true).to_clock(); + let clock = ClockControl::new_auto_advancing().to_clock(); HttpClient::builder_fake(status, FakeDeps { clock }) .custom_pipeline(move |dispatch, context| { let layer = HttpRetry::layer("dummy", context.resilience_context()) @@ -166,7 +166,7 @@ async fn breaker_rejected_request_error_attaches_request() { fn create_breaker_client(status: StatusCode) -> HttpClient { let handler = FakeHandler::from(HttpResponseBuilder::new_fake().status(status).build().unwrap()); - let clock = ClockControl::default().auto_advance_timers(true).to_clock(); + let clock = ClockControl::new_auto_advancing().to_clock(); HttpClient::builder_fake(handler, FakeDeps { clock }) .custom_pipeline(move |dispatch, context| { let layer = HttpBreaker::layer("test", context.resilience_context()) diff --git a/crates/fetch/tests/standard_pipeline.rs b/crates/fetch/tests/standard_pipeline.rs index aa88ebf99..66738da09 100644 --- a/crates/fetch/tests/standard_pipeline.rs +++ b/crates/fetch/tests/standard_pipeline.rs @@ -72,7 +72,7 @@ fn create_per_host_client(calls: Calls) -> HttpClient { HttpResponseBuilder::new_fake().status(status).build() }); - let clock = ClockControl::default().auto_advance_timers(true).to_clock(); + let clock = ClockControl::new_auto_advancing().to_clock(); HttpClient::builder_fake(handler, FakeDeps { clock }) .standard_pipeline(|pipeline, _| { pipeline @@ -98,7 +98,7 @@ fn create_uniform_client(status: StatusCode, calls: Calls) -> HttpClient { HttpResponseBuilder::new_fake().status(status).build() }); - let clock = ClockControl::default().auto_advance_timers(true).to_clock(); + let clock = ClockControl::new_auto_advancing().to_clock(); HttpClient::builder_fake(handler, FakeDeps { clock }) .standard_pipeline(|pipeline, _| { pipeline @@ -284,7 +284,7 @@ const HEDGING_DELAY: Duration = Duration::from_millis(100); /// Creates a hedging client whose handler returns status codes from the /// given iterator in order. fn create_hedging_client(calls: Calls, responses: Vec) -> HttpClient { - let clock = ClockControl::default().auto_advance_timers(true).to_clock(); + let clock = ClockControl::new_auto_advancing().to_clock(); let responses = Arc::new(std::sync::Mutex::new(responses.into_iter())); let handler = FakeHandler::from_fn(move |_req| { @@ -368,7 +368,7 @@ async fn fallback_router_recovers_when_primary_is_unavailable() { other => panic!("unexpected host: {other:?}"), }); - let clock = ClockControl::default().auto_advance_timers(true).to_clock(); + let clock = ClockControl::new_auto_advancing().to_clock(); let client = HttpClient::builder_fake(handler, FakeDeps { clock }) .router(Router::fallback( BaseUri::from_static("https://primary.example.com/"), diff --git a/crates/fetch/tests/timeout.rs b/crates/fetch/tests/timeout.rs index 79fadb012..15f1abd87 100644 --- a/crates/fetch/tests/timeout.rs +++ b/crates/fetch/tests/timeout.rs @@ -15,7 +15,7 @@ use tick::ClockControl; #[tokio::test] async fn response_timeout() { let handler = FakeHandler::never_completes(); - let clock = ClockControl::new().auto_advance_timers(true).to_clock(); + let clock = ClockControl::new_auto_advancing().to_clock(); let client = HttpClient::builder_fake(handler, FakeDeps { clock }).minimal_pipeline().build(); let err = client diff --git a/crates/fetch_hyper/src/builder.rs b/crates/fetch_hyper/src/builder.rs index db7803ac5..edda28ae9 100644 --- a/crates/fetch_hyper/src/builder.rs +++ b/crates/fetch_hyper/src/builder.rs @@ -100,9 +100,7 @@ where /// let transport: HyperTransport = HyperTransportBuilder::new( /// Execute::new(connect), /// Spawner::new_tokio(), -/// tick::ClockControl::new() -/// .auto_advance_timers(true) -/// .to_clock(), +/// tick::ClockControl::new_auto_advancing().to_clock(), /// TransportOptions::default(), /// ) /// .build(tls); @@ -299,9 +297,9 @@ mod tests { fn make_builder_with(options: TransportOptions) -> HyperTransportBuilder { HyperTransportBuilder::new( - FakeConnector::new_success(Bytes::new(), tick::ClockControl::new().auto_advance_timers(true).to_clock()), + FakeConnector::new_success(Bytes::new(), tick::ClockControl::new_auto_advancing().to_clock()), Spawner::new_tokio(), - tick::ClockControl::new().auto_advance_timers(true).to_clock(), + tick::ClockControl::new_auto_advancing().to_clock(), options, ) } @@ -392,7 +390,7 @@ mod tests { async fn build_with_explicit_meter_yields_working_transport() { let provider = SdkMeterProvider::builder().build(); let response_bytes = Bytes::from_static(b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n"); - let clock = tick::ClockControl::new().auto_advance_timers(true).to_clock(); + let clock = tick::ClockControl::new_auto_advancing().to_clock(); let handler = HyperTransportBuilder::new( FakeConnector::new_success(response_bytes, clock.clone()), Spawner::new_tokio(), @@ -412,7 +410,7 @@ mod tests { // We can't easily inspect hyper's internal flag, but we can at least // exercise the build path with HTTP/2-only configuration to confirm // it succeeds without panicking. - let clock = tick::ClockControl::new().auto_advance_timers(true).to_clock(); + let clock = tick::ClockControl::new_auto_advancing().to_clock(); let mut options = TransportOptions::default(); options.supported_http_versions = vec![Version::HTTP_2]; let _handler = HyperTransportBuilder::new( @@ -428,7 +426,7 @@ mod tests { #[cfg_attr(miri, ignore)] #[tokio::test] async fn hyper_transport_clones_share_underlying_service() { - let clock = tick::ClockControl::new().auto_advance_timers(true).to_clock(); + let clock = tick::ClockControl::new_auto_advancing().to_clock(); let response_bytes = Bytes::from_static(b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n"); let handler = HyperTransportBuilder::new( FakeConnector::new_success(response_bytes, clock.clone()), @@ -447,7 +445,7 @@ mod tests { #[cfg_attr(miri, ignore)] #[tokio::test] async fn hyper_transport_into_dynamic_service_executes_request() { - let clock = tick::ClockControl::new().auto_advance_timers(true).to_clock(); + let clock = tick::ClockControl::new_auto_advancing().to_clock(); let response_bytes = Bytes::from_static(b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n"); let handler = HyperTransportBuilder::new( FakeConnector::new_success(response_bytes, clock.clone()), @@ -470,7 +468,7 @@ mod tests { // is provided, `build` synthesizes one and still produces a working // transport. let response_bytes = Bytes::from_static(b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n"); - let clock = tick::ClockControl::new().auto_advance_timers(true).to_clock(); + let clock = tick::ClockControl::new_auto_advancing().to_clock(); let handler = HyperTransportBuilder::new( FakeConnector::new_success(response_bytes, clock.clone()), Spawner::new_tokio(), diff --git a/crates/fetch_hyper/src/connection/client_connector.rs b/crates/fetch_hyper/src/connection/client_connector.rs index 6e225916e..5e8ba09f3 100644 --- a/crates/fetch_hyper/src/connection/client_connector.rs +++ b/crates/fetch_hyper/src/connection/client_connector.rs @@ -334,7 +334,7 @@ mod tests { .f64_histogram("http.client.connection.setup.duration") .build(); - let clock = tick::ClockControl::new().auto_advance_timers(true).to_clock(); + let clock = tick::ClockControl::new_auto_advancing().to_clock(); let connector = FakeConnector::new_success(Bytes::new(), clock.clone()); let base = BaseUri::from_static("http://example.com"); let result = connect_with_timeout( @@ -361,7 +361,7 @@ mod tests { .f64_histogram("http.client.connection.setup.duration") .build(); - let clock = tick::ClockControl::new().auto_advance_timers(true).to_clock(); + let clock = tick::ClockControl::new_auto_advancing().to_clock(); let connector = FakeConnector::new_connect_failure(TestError::new("boom"), clock.clone()); let base = BaseUri::from_static("http://example.com"); let err = connect_with_timeout( @@ -389,7 +389,7 @@ mod tests { .f64_histogram("http.client.connection.setup.duration") .build(); - let control = tick::ClockControl::new().auto_advance_timers(true); + let control = tick::ClockControl::new_auto_advancing(); let clock = control.to_clock(); let base = BaseUri::from_static("http://example.com"); // pending() never resolves, so the timeout always wins. @@ -411,7 +411,7 @@ mod tests { use crate::testing::FakeConnector; - let clock = tick::ClockControl::new().auto_advance_timers(true).to_clock(); + let clock = tick::ClockControl::new_auto_advancing().to_clock(); let connector = FakeConnector::new_success(Bytes::new(), clock.clone()); let provider = SdkMeterProvider::builder().build(); let meter = provider.meter("test"); @@ -436,7 +436,7 @@ mod tests { use crate::testing::FakeConnector; - let clock = tick::ClockControl::new().auto_advance_timers(true).to_clock(); + let clock = tick::ClockControl::new_auto_advancing().to_clock(); let connector = FakeConnector::new_success(Bytes::new(), clock.clone()); let provider = SdkMeterProvider::builder().build(); let meter = provider.meter("test"); diff --git a/crates/fetch_hyper/src/connection/hyper_connector_adapter.rs b/crates/fetch_hyper/src/connection/hyper_connector_adapter.rs index 0f0f752f4..48006bbb1 100644 --- a/crates/fetch_hyper/src/connection/hyper_connector_adapter.rs +++ b/crates/fetch_hyper/src/connection/hyper_connector_adapter.rs @@ -81,7 +81,7 @@ mod tests { async fn call_translates_uri_into_base_uri_and_invokes_connector() { let mut adapter = HyperConnectorAdapter::new(FakeConnector::new_success( Bytes::from_static(b""), - tick::ClockControl::new().auto_advance_timers(true).to_clock(), + tick::ClockControl::new_auto_advancing().to_clock(), )); adapter.call(Uri::from_static("https://example.com/")).await.unwrap(); } @@ -91,7 +91,7 @@ mod tests { async fn call_propagates_invalid_uri_error() { let mut adapter = HyperConnectorAdapter::new(FakeConnector::new_success( Bytes::from_static(b""), - tick::ClockControl::new().auto_advance_timers(true).to_clock(), + tick::ClockControl::new_auto_advancing().to_clock(), )); // A relative URI (no scheme/authority) is not a valid BaseUri. adapter diff --git a/crates/fetch_hyper/src/connection/hyper_handler.rs b/crates/fetch_hyper/src/connection/hyper_handler.rs index 73d3c7b90..f6365bb64 100644 --- a/crates/fetch_hyper/src/connection/hyper_handler.rs +++ b/crates/fetch_hyper/src/connection/hyper_handler.rs @@ -209,7 +209,7 @@ mod tests { } fn make_handler(connector: FakeConnector, lifetime: ConnectionLifetime) -> HyperTransport { - let clock = tick::ClockControl::new().auto_advance_timers(true).to_clock(); + let clock = tick::ClockControl::new_auto_advancing().to_clock(); let mut options = fetch_options::TransportOptions::default(); options.request_filter = RequestFilter::HttpAndHttps; options.connection_pool.connection_lifetime = lifetime; @@ -228,7 +228,7 @@ mod tests { #[test] #[cfg_attr(miri, ignore)] fn debug_renders_handler_type() { - let clock = tick::ClockControl::new().auto_advance_timers(true).to_clock(); + let clock = tick::ClockControl::new_auto_advancing().to_clock(); let connector = FakeConnector::new_success(http_response_bytes(), clock.clone()); let mut options = fetch_options::TransportOptions::default(); options.request_filter = RequestFilter::HttpAndHttps; @@ -248,7 +248,7 @@ mod tests { // The byte stream is not a valid HTTP/1 response, so hyper's client // request future fails with a `legacy::Error`, exercising // `create_http_error_from_hyper_util`. - let clock = tick::ClockControl::new().auto_advance_timers(true).to_clock(); + let clock = tick::ClockControl::new_auto_advancing().to_clock(); let connector = FakeConnector::new_success(Bytes::from_static(b"NOT A VALID HTTP RESPONSE"), clock.clone()); let handler = make_handler(connector, ConnectionLifetime::unlimited()); let err = handler.execute(test_request()).await.expect_err("expected error"); @@ -261,7 +261,7 @@ mod tests { // Builder with HTTP/2-only flips `http2_only(true)` on hyper's builder. // Using FakeStream over HTTP/1.1-style data will fail, but we want to // simply exercise the build path and request execution. - let clock = tick::ClockControl::new().auto_advance_timers(true).to_clock(); + let clock = tick::ClockControl::new_auto_advancing().to_clock(); let connector = FakeConnector::new_success(http_response_bytes(), clock.clone()); let mut options = fetch_options::TransportOptions::default(); options.request_filter = RequestFilter::HttpAndHttps; @@ -320,7 +320,7 @@ mod tests { #[cfg_attr(miri, ignore)] #[tokio::test] async fn end_to_end_response_is_returned_with_body() { - let clock = tick::ClockControl::new().auto_advance_timers(true).to_clock(); + let clock = tick::ClockControl::new_auto_advancing().to_clock(); let connector = FakeConnector::new_success(http_response_bytes(), clock.clone()); let handler = make_handler(connector, ConnectionLifetime::unlimited()); let resp = handler.execute(test_request()).await.unwrap(); diff --git a/crates/fetch_hyper/src/testing.rs b/crates/fetch_hyper/src/testing.rs index 27c1bcda3..d5152e45f 100644 --- a/crates/fetch_hyper/src/testing.rs +++ b/crates/fetch_hyper/src/testing.rs @@ -374,7 +374,7 @@ mod tests { #[cfg_attr(miri, ignore)] #[tokio::test] async fn fake_connector_serves_canned_response() { - let clock = tick::ClockControl::new().auto_advance_timers(true).to_clock(); + let clock = tick::ClockControl::new_auto_advancing().to_clock(); let mut options = fetch_options::TransportOptions::default(); options.request_filter = RequestFilter::HttpAndHttps; let handler = HyperTransportBuilder::new( @@ -396,7 +396,7 @@ mod tests { #[cfg_attr(miri, ignore)] #[tokio::test] async fn fake_connector_propagates_connect_failure() { - let clock = tick::ClockControl::new().auto_advance_timers(true).to_clock(); + let clock = tick::ClockControl::new_auto_advancing().to_clock(); let mut options = fetch_options::TransportOptions::default(); options.request_filter = RequestFilter::HttpAndHttps; options.connect_timeout = Duration::from_secs(5); @@ -427,7 +427,7 @@ mod tests { #[cfg_attr(miri, ignore)] #[tokio::test] async fn https_only_filter_rejects_http_request() { - let clock = tick::ClockControl::new().auto_advance_timers(true).to_clock(); + let clock = tick::ClockControl::new_auto_advancing().to_clock(); let handler = HyperTransportBuilder::new( FakeConnector::new_success(http_1_response(), clock.clone()), Spawner::new_tokio(), diff --git a/crates/fetch_hyper/src/timer.rs b/crates/fetch_hyper/src/timer.rs index 41d9176c7..8478fb2cb 100644 --- a/crates/fetch_hyper/src/timer.rs +++ b/crates/fetch_hyper/src/timer.rs @@ -56,7 +56,7 @@ mod tests { #[cfg_attr(miri, ignore)] #[tokio::test] async fn sleep_advances_by_duration() { - let clock = ClockControl::new().auto_advance_timers(true).to_clock(); + let clock = ClockControl::new_auto_advancing().to_clock(); let timer = ClockTimer::new(clock.clone()); let watch = clock.stopwatch(); @@ -67,7 +67,7 @@ mod tests { #[cfg_attr(miri, ignore)] #[tokio::test] async fn sleep_until_advances_to_deadline() { - let clock = ClockControl::new().auto_advance_timers(true).to_clock(); + let clock = ClockControl::new_auto_advancing().to_clock(); let timer = ClockTimer::new(clock.clone()); let now = clock.instant(); @@ -79,7 +79,7 @@ mod tests { #[cfg_attr(miri, ignore)] #[tokio::test] async fn sleep_until_past_deadline_returns_immediately() { - let clock = ClockControl::new().auto_advance_timers(true).to_clock(); + let clock = ClockControl::new_auto_advancing().to_clock(); let timer = ClockTimer::new(clock.clone()); let watch = clock.stopwatch(); diff --git a/crates/fetch_hyper/src/tls/connector.rs b/crates/fetch_hyper/src/tls/connector.rs index 14ef50717..79dec658e 100644 --- a/crates/fetch_hyper/src/tls/connector.rs +++ b/crates/fetch_hyper/src/tls/connector.rs @@ -331,7 +331,7 @@ mod tests { #[cfg_attr(miri, ignore)] #[tokio::test] async fn execute_native_tls_propagates_connector_error() { - let clock = tick::ClockControl::new().auto_advance_timers(true).to_clock(); + let clock = tick::ClockControl::new_auto_advancing().to_clock(); let connector = FakeConnector::new_connect_failure(TestError::new("fail"), clock); let c: TlsConnector = TlsConnector::new(native_tls_backend(), connector, RequestFilter::HttpAndHttps, &[Version::HTTP_11]); @@ -345,7 +345,7 @@ mod tests { #[cfg_attr(miri, ignore)] #[tokio::test] async fn execute_rustls_propagates_connector_error() { - let clock = tick::ClockControl::new().auto_advance_timers(true).to_clock(); + let clock = tick::ClockControl::new_auto_advancing().to_clock(); let connector = FakeConnector::new_connect_failure(TestError::new("fail-rustls"), clock); let c: TlsConnector = TlsConnector::new( rustls_backend(), diff --git a/crates/fetch_hyper/tests/smoke.rs b/crates/fetch_hyper/tests/smoke.rs index dc1cfe270..beff924d4 100644 --- a/crates/fetch_hyper/tests/smoke.rs +++ b/crates/fetch_hyper/tests/smoke.rs @@ -41,7 +41,7 @@ fn build_tls() -> TlsBackend { } fn test_clock() -> Clock { - ClockControl::new().auto_advance_timers(true).to_clock() + ClockControl::new_auto_advancing().to_clock() } /// Builds [`TransportOptions`] with `HttpAndHttps`, a 5s connect timeout, and diff --git a/crates/http_extensions/src/body/timeout_body.rs b/crates/http_extensions/src/body/timeout_body.rs index c3f59fadc..c8ffb9b5a 100644 --- a/crates/http_extensions/src/body/timeout_body.rs +++ b/crates/http_extensions/src/body/timeout_body.rs @@ -120,7 +120,7 @@ mod tests { #[test] fn stream_body_times_out_when_pending() { - let clock = ClockControl::new().auto_advance_timers(true).to_clock(); + let clock = ClockControl::new_auto_advancing().to_clock(); let builder = HttpBodyBuilder::new(GlobalPool::new(), &clock); // A body that never yields data. @@ -135,7 +135,7 @@ mod tests { #[test] fn body_timeout_chains_with_buffer_limit() { - let clock = ClockControl::new().auto_advance_timers(true).to_clock(); + let clock = ClockControl::new_auto_advancing().to_clock(); let builder = HttpBodyBuilder::new(GlobalPool::new(), &clock).with_options(HttpBodyOptions::default().buffer_limit(1024)); assert_eq!(builder.options, HttpBodyOptions::default().buffer_limit(1024)); @@ -209,7 +209,7 @@ mod tests { #[test] fn poll_frame_times_out_when_pending_with_short_timeout() { - let clock = ClockControl::new().auto_advance_timers(true).to_clock(); + let clock = ClockControl::new_auto_advancing().to_clock(); let builder = HttpBodyBuilder::new(GlobalPool::new(), &clock); // A body that never yields data with a very short timeout. @@ -273,7 +273,7 @@ mod tests { #[test] fn poll_frame_returns_error_after_timeout() { - let clock = ClockControl::new().auto_advance_timers(true).to_clock(); + let clock = ClockControl::new_auto_advancing().to_clock(); let timeout = Duration::from_millis(50); let mut timeout_body = super::TimeoutBody::new(PendingBody, timeout, &clock); diff --git a/crates/http_extensions/src/fake_handler.rs b/crates/http_extensions/src/fake_handler.rs index c4d51d733..906eab6d7 100644 --- a/crates/http_extensions/src/fake_handler.rs +++ b/crates/http_extensions/src/fake_handler.rs @@ -481,7 +481,7 @@ mod tests { #[test] fn never_completes_handler() { let handler = FakeHandler::never_completes(); - let clock = ClockControl::new().auto_advance_timers(true).to_clock(); + let clock = ClockControl::new_auto_advancing().to_clock(); let error = block_on( handler .request_builder() diff --git a/crates/seatbelt/src/chaos/latency/service.rs b/crates/seatbelt/src/chaos/latency/service.rs index 55ed8a572..8db304167 100644 --- a/crates/seatbelt/src/chaos/latency/service.rs +++ b/crates/seatbelt/src/chaos/latency/service.rs @@ -282,7 +282,10 @@ mod tests { let log_capture = Capture::new(); let _guard = log_capture.subscriber().set_default(); - let clock = tick::ClockControl::default().auto_advance(Duration::from_millis(200)).to_clock(); + let clock = tick::ClockControl::builder() + .auto_advance(Duration::from_millis(200)) + .build() + .to_clock(); let context = ResilienceContext::new(&clock).use_logs().name("log_test_pipeline"); let stack = ( @@ -310,7 +313,10 @@ mod tests { use crate::utils::{EVENT_NAME, PIPELINE_NAME, STRATEGY_NAME}; let metrics = MetricTester::new(); - let clock = tick::ClockControl::default().auto_advance(Duration::from_millis(200)).to_clock(); + let clock = tick::ClockControl::builder() + .auto_advance(Duration::from_millis(200)) + .build() + .to_clock(); let context = ResilienceContext::new(&clock) .use_metrics(metrics.meter_provider()) .name("metrics_pipeline"); diff --git a/crates/seatbelt/src/hedging/service.rs b/crates/seatbelt/src/hedging/service.rs index 988182c49..69c579b4a 100644 --- a/crates/seatbelt/src/hedging/service.rs +++ b/crates/seatbelt/src/hedging/service.rs @@ -372,7 +372,7 @@ mod tests { #[cfg_attr(miri, ignore)] async fn hedging_emits_metrics() { let tester = MetricTester::new(); - let context = ResilienceContext::::new(ClockControl::default().auto_advance_timers(true).to_clock()) + let context = ResilienceContext::::new(ClockControl::new_auto_advancing().to_clock()) .name("test_pipeline") .use_metrics(tester.meter_provider()); @@ -410,7 +410,7 @@ mod tests { let log_capture = Capture::new(); let _guard = log_capture.subscriber().set_default(); - let clock = ClockControl::default().auto_advance_timers(true).to_clock(); + let clock = ClockControl::new_auto_advancing().to_clock(); let context = ResilienceContext::::new(clock).name("log_test_pipeline").use_logs(); let service = Hedging::layer("log_test_hedging", &context) @@ -492,7 +492,7 @@ mod tests { use tokio::sync::Notify; let tester = MetricTester::new(); - let clock = ClockControl::default().auto_advance_timers(true).to_clock(); + let clock = ClockControl::new_auto_advancing().to_clock(); let context = ResilienceContext::>::new(clock) .name("test_pipeline") .use_metrics(tester.meter_provider()); diff --git a/crates/seatbelt/src/retry/service.rs b/crates/seatbelt/src/retry/service.rs index 77dc080c3..24d51ab57 100644 --- a/crates/seatbelt/src/retry/service.rs +++ b/crates/seatbelt/src/retry/service.rs @@ -375,7 +375,7 @@ mod tests { #[tokio::test] async fn retries_exhausted_ensure_telemetry_reported() { let tester = MetricTester::new(); - let context = ResilienceContext::::new(ClockControl::default().auto_advance_timers(true).to_clock()) + let context = ResilienceContext::::new(ClockControl::new_auto_advancing().to_clock()) .name("test_pipeline") .use_metrics(tester.meter_provider()); @@ -413,7 +413,7 @@ mod tests { let log_capture = Capture::new(); let _guard = log_capture.subscriber().set_default(); - let clock = ClockControl::default().auto_advance_timers(true).to_clock(); + let clock = ClockControl::new_auto_advancing().to_clock(); let context = ResilienceContext::::new(clock).name("log_test_pipeline").use_logs(); let service = Retry::layer("log_test_retry", &context) diff --git a/crates/seatbelt/src/timeout/service.rs b/crates/seatbelt/src/timeout/service.rs index 051c492cb..5ab3f6dd6 100644 --- a/crates/seatbelt/src/timeout/service.rs +++ b/crates/seatbelt/src/timeout/service.rs @@ -252,9 +252,10 @@ mod tests { let log_capture = Capture::new(); let _guard = log_capture.subscriber().set_default(); - let clock = ClockControl::default() + let clock = ClockControl::builder() .auto_advance(Duration::from_millis(200)) .auto_advance_limit(Duration::from_millis(500)) + .build() .to_clock(); let context = ResilienceContext::new(clock.clone()).use_logs().name("log_test_pipeline"); @@ -289,9 +290,10 @@ mod tests { use crate::utils::{EVENT_NAME, PIPELINE_NAME, STRATEGY_NAME}; let metrics = MetricTester::new(); - let clock = ClockControl::default() + let clock = ClockControl::builder() .auto_advance(Duration::from_millis(200)) .auto_advance_limit(Duration::from_millis(500)) + .build() .to_clock(); let context = ResilienceContext::new(clock.clone()) .use_metrics(metrics.meter_provider()) diff --git a/crates/seatbelt/tests/chaos_latency.rs b/crates/seatbelt/tests/chaos_latency.rs index 0d25036f1..0cbba0dd1 100644 --- a/crates/seatbelt/tests/chaos_latency.rs +++ b/crates/seatbelt/tests/chaos_latency.rs @@ -40,7 +40,7 @@ where #[case::tower(true)] #[tokio::test] async fn no_latency_when_rate_zero(#[case] use_tower: bool) { - let clock = ClockControl::default().auto_advance_timers(true).to_clock(); + let clock = ClockControl::new_auto_advancing().to_clock(); let context = ResilienceContext::new(&clock); let stack = ( @@ -59,7 +59,7 @@ async fn no_latency_when_rate_zero(#[case] use_tower: bool) { #[case::tower(true)] #[tokio::test] async fn always_latency_when_rate_one(#[case] use_tower: bool) { - let clock = ClockControl::default().auto_advance_timers(true).to_clock(); + let clock = ClockControl::new_auto_advancing().to_clock(); let context = ResilienceContext::new(&clock); let stopwatch = clock.stopwatch(); @@ -84,7 +84,7 @@ async fn always_latency_when_rate_one(#[case] use_tower: bool) { #[case::tower(true)] #[tokio::test] async fn latency_with_dynamic_duration(#[case] use_tower: bool) { - let clock = ClockControl::default().auto_advance_timers(true).to_clock(); + let clock = ClockControl::new_auto_advancing().to_clock(); let context = ResilienceContext::new(&clock); let stack = ( @@ -114,7 +114,7 @@ async fn latency_with_dynamic_duration(#[case] use_tower: bool) { #[case::tower(true)] #[tokio::test] async fn no_latency_if_disabled(#[case] use_tower: bool) { - let clock = ClockControl::default().auto_advance_timers(true).to_clock(); + let clock = ClockControl::new_auto_advancing().to_clock(); let context = ResilienceContext::new(&clock); let stack = ( @@ -137,7 +137,7 @@ async fn no_latency_if_disabled(#[case] use_tower: bool) { #[case::tower(true)] #[tokio::test] async fn enable_if_respected(#[case] use_tower: bool) { - let clock = ClockControl::default().auto_advance_timers(true).to_clock(); + let clock = ClockControl::new_auto_advancing().to_clock(); let context = ResilienceContext::new(&clock); let stack = ( @@ -164,7 +164,7 @@ async fn enable_if_respected(#[case] use_tower: bool) { #[case::tower(true)] #[tokio::test] async fn clone_service_works_independently(#[case] use_tower: bool) { - let clock = ClockControl::default().auto_advance_timers(true).to_clock(); + let clock = ClockControl::new_auto_advancing().to_clock(); let context: ResilienceContext> = ResilienceContext::new(&clock).name("test_pipeline"); let stack = ( @@ -189,7 +189,7 @@ async fn clone_service_works_independently(#[case] use_tower: bool) { #[case::tower(true)] #[tokio::test] async fn config_applies_rate_and_latency(#[case] use_tower: bool) { - let clock = ClockControl::default().auto_advance_timers(true).to_clock(); + let clock = ClockControl::new_auto_advancing().to_clock(); let context = ResilienceContext::new(&clock); let mut config = LatencyConfig::default(); @@ -216,7 +216,7 @@ async fn config_applies_rate_and_latency(#[case] use_tower: bool) { #[case::tower(true)] #[tokio::test] async fn config_disabled_passes_through(#[case] use_tower: bool) { - let clock = ClockControl::default().auto_advance_timers(true).to_clock(); + let clock = ClockControl::new_auto_advancing().to_clock(); let context = ResilienceContext::new(&clock); let mut config = LatencyConfig::default(); @@ -240,7 +240,7 @@ async fn config_disabled_passes_through(#[case] use_tower: bool) { #[case::tower(true)] #[tokio::test] async fn config_with_max_latency_creates_range(#[case] use_tower: bool) { - let clock = ClockControl::default().auto_advance_timers(true).to_clock(); + let clock = ClockControl::new_auto_advancing().to_clock(); let context = ResilienceContext::new(&clock); let mut config = LatencyConfig::default(); @@ -271,7 +271,7 @@ async fn config_with_max_latency_creates_range(#[case] use_tower: bool) { #[case::tower(true)] #[tokio::test] async fn rate_with_dynamic_rate(#[case] use_tower: bool) { - let clock = ClockControl::default().auto_advance_timers(true).to_clock(); + let clock = ClockControl::new_auto_advancing().to_clock(); let context = ResilienceContext::new(&clock); let stack = ( @@ -297,7 +297,7 @@ async fn rate_with_dynamic_rate(#[case] use_tower: bool) { #[case::tower(true)] #[tokio::test] async fn rate_with_clamps_above_one(#[case] use_tower: bool) { - let clock = ClockControl::default().auto_advance_timers(true).to_clock(); + let clock = ClockControl::new_auto_advancing().to_clock(); let context = ResilienceContext::new(&clock); let stack = ( @@ -320,7 +320,7 @@ async fn rate_with_clamps_above_one(#[case] use_tower: bool) { #[case::tower(true)] #[tokio::test] async fn latency_range_produces_delay_within_bounds(#[case] use_tower: bool) { - let clock = ClockControl::default().auto_advance_timers(true).to_clock(); + let clock = ClockControl::new_auto_advancing().to_clock(); let context = ResilienceContext::new(&clock); let stack = ( @@ -346,7 +346,7 @@ async fn latency_range_produces_delay_within_bounds(#[case] use_tower: bool) { #[case::tower(true)] #[tokio::test] async fn inner_service_output_preserved(#[case] use_tower: bool) { - let clock = ClockControl::default().auto_advance_timers(true).to_clock(); + let clock = ClockControl::new_auto_advancing().to_clock(); let context = ResilienceContext::new(&clock); let stack = ( diff --git a/crates/seatbelt/tests/hedging.rs b/crates/seatbelt/tests/hedging.rs index 8f303725a..2acb3d31d 100644 --- a/crates/seatbelt/tests/hedging.rs +++ b/crates/seatbelt/tests/hedging.rs @@ -70,7 +70,7 @@ async fn hedging_disabled_passes_through(#[case] use_tower: bool) { #[case::tower(true)] #[tokio::test] async fn immediate_mode_all_run_concurrently(#[case] use_tower: bool) { - let clock = ClockControl::default().auto_advance_timers(true).to_clock(); + let clock = ClockControl::new_auto_advancing().to_clock(); let counter = Arc::new(AtomicU32::new(0)); let counter_clone = Arc::clone(&counter); @@ -109,7 +109,7 @@ async fn immediate_mode_all_run_concurrently(#[case] use_tower: bool) { #[case::tower(true)] #[tokio::test] async fn immediate_mode_returns_first_success(#[case] use_tower: bool) { - let clock = ClockControl::default().auto_advance_timers(true).to_clock(); + let clock = ClockControl::new_auto_advancing().to_clock(); let counter = Arc::new(AtomicU32::new(0)); let counter_clone = Arc::clone(&counter); @@ -141,7 +141,7 @@ async fn immediate_mode_returns_first_success(#[case] use_tower: bool) { #[case::tower(true)] #[tokio::test] async fn delay_mode_launches_hedging_attempt_after_timeout(#[case] use_tower: bool) { - let clock = ClockControl::default().auto_advance_timers(true).to_clock(); + let clock = ClockControl::new_auto_advancing().to_clock(); let counter = Arc::new(AtomicU32::new(0)); let counter_clone = Arc::clone(&counter); @@ -180,7 +180,7 @@ async fn delay_mode_launches_hedging_attempt_after_timeout(#[case] use_tower: bo #[case::tower(true)] #[tokio::test] async fn dynamic_mode_computes_delay(#[case] use_tower: bool) { - let clock = ClockControl::default().auto_advance_timers(true).to_clock(); + let clock = ClockControl::new_auto_advancing().to_clock(); let counter = Arc::new(AtomicU32::new(0)); let counter_clone = Arc::clone(&counter); @@ -218,7 +218,7 @@ async fn dynamic_mode_computes_delay(#[case] use_tower: bool) { #[case::tower(true)] #[tokio::test] async fn on_execute_callback_invoked(#[case] use_tower: bool) { - let clock = ClockControl::default().auto_advance_timers(true).to_clock(); + let clock = ClockControl::new_auto_advancing().to_clock(); let execute_calls = Arc::new(AtomicU32::new(0)); let execute_calls_clone = Arc::clone(&execute_calls); @@ -275,7 +275,7 @@ async fn no_hedging_configured_passes_through(#[case] use_tower: bool) { #[case::tower(true)] #[tokio::test] async fn all_fail_returns_last_result(#[case] use_tower: bool) { - let clock = ClockControl::default().auto_advance_timers(true).to_clock(); + let clock = ClockControl::new_auto_advancing().to_clock(); let context: ResilienceContext> = ResilienceContext::new(&clock).name("test"); let stack = ( @@ -298,7 +298,7 @@ async fn all_fail_returns_last_result(#[case] use_tower: bool) { #[case::tower(true)] #[tokio::test] async fn clone_service_works_independently(#[case] use_tower: bool) { - let clock = ClockControl::default().auto_advance_timers(true).to_clock(); + let clock = ClockControl::new_auto_advancing().to_clock(); let call_count = Arc::new(AtomicU32::new(0)); let call_count_clone = Arc::clone(&call_count); @@ -333,7 +333,7 @@ async fn clone_service_works_independently(#[case] use_tower: bool) { #[case::tower(true)] #[tokio::test] async fn enable_if_skips_hedging(#[case] use_tower: bool) { - let clock = ClockControl::default().auto_advance_timers(true).to_clock(); + let clock = ClockControl::new_auto_advancing().to_clock(); let counter = Arc::new(AtomicU32::new(0)); let counter_clone = Arc::clone(&counter); @@ -364,7 +364,7 @@ async fn enable_if_skips_hedging(#[case] use_tower: bool) { #[case::tower(true)] #[tokio::test] async fn clone_returning_none_skips_hedging(#[case] use_tower: bool) { - let clock = ClockControl::default().auto_advance_timers(true).to_clock(); + let clock = ClockControl::new_auto_advancing().to_clock(); let counter = Arc::new(AtomicU32::new(0)); let counter_clone = Arc::clone(&counter); @@ -406,7 +406,7 @@ async fn clone_returning_none_skips_hedging(#[case] use_tower: bool) { #[case::tower(true)] #[tokio::test] async fn handle_unavailable_continues_hedging(#[case] use_tower: bool) { - let clock = ClockControl::default().auto_advance_timers(true).to_clock(); + let clock = ClockControl::new_auto_advancing().to_clock(); let counter = Arc::new(AtomicU32::new(0)); let counter_clone = Arc::clone(&counter); @@ -452,7 +452,7 @@ async fn slow_original_accepted_after_hedging_launched(#[case] use_tower: bool) // original finishes first and its result is accepted. use tokio::sync::Notify; - let clock = ClockControl::default().auto_advance_timers(true).to_clock(); + let clock = ClockControl::new_auto_advancing().to_clock(); let counter = Arc::new(AtomicU32::new(0)); let counter_clone = Arc::clone(&counter); @@ -512,7 +512,7 @@ async fn slow_original_accepted_after_hedging_launched(#[case] use_tower: bool) #[case::tower(true)] #[tokio::test] async fn non_cloneable_input_skips_hedging_and_invokes_on_execute(#[case] use_tower: bool) { - let clock = ClockControl::default().auto_advance_timers(true).to_clock(); + let clock = ClockControl::new_auto_advancing().to_clock(); let counter = Arc::new(AtomicU32::new(0)); let counter_clone = Arc::clone(&counter); let execute_calls = Arc::new(AtomicU32::new(0)); @@ -550,7 +550,7 @@ async fn non_cloneable_input_skips_hedging_and_invokes_on_execute(#[case] use_to #[case::tower(true)] #[tokio::test] async fn initial_attempt_is_last_when_no_hedging_attempts(#[case] use_tower: bool) { - let clock = ClockControl::default().auto_advance_timers(true).to_clock(); + let clock = ClockControl::new_auto_advancing().to_clock(); let observed_is_last = Arc::new(AtomicBool::new(false)); let observed_clone = Arc::clone(&observed_is_last); @@ -580,7 +580,7 @@ async fn initial_attempt_is_last_when_no_hedging_attempts(#[case] use_tower: boo #[case::tower(true)] #[tokio::test] async fn unavailable_returned_immediately_when_handle_unavailable_is_false(#[case] use_tower: bool) { - let clock = ClockControl::default().auto_advance_timers(true).to_clock(); + let clock = ClockControl::new_auto_advancing().to_clock(); let counter = Arc::new(AtomicU32::new(0)); let counter_clone = Arc::clone(&counter); @@ -621,7 +621,7 @@ async fn unavailable_returned_immediately_when_handle_unavailable_is_false(#[cas #[case::tower(true)] #[tokio::test] async fn launch_hedging_attempt_actually_launches_attempts(#[case] use_tower: bool) { - let clock = ClockControl::default().auto_advance_timers(true).to_clock(); + let clock = ClockControl::new_auto_advancing().to_clock(); let counter = Arc::new(AtomicU32::new(0)); let counter_clone = Arc::clone(&counter); @@ -662,7 +662,7 @@ async fn launch_hedging_attempt_actually_launches_attempts(#[case] use_tower: bo #[case::tower(true)] #[tokio::test] async fn invoke_on_execute_actually_calls_callback(#[case] use_tower: bool) { - let clock = ClockControl::default().auto_advance_timers(true).to_clock(); + let clock = ClockControl::new_auto_advancing().to_clock(); let execute_calls = Arc::new(AtomicU32::new(0)); let execute_calls_clone = Arc::clone(&execute_calls); diff --git a/crates/seatbelt/tests/retry.rs b/crates/seatbelt/tests/retry.rs index df4e558c0..a7e48c33d 100644 --- a/crates/seatbelt/tests/retry.rs +++ b/crates/seatbelt/tests/retry.rs @@ -124,7 +124,7 @@ async fn no_recovery_ensure_no_additional_retries(#[case] use_tower: bool) { #[case::tower(true)] #[tokio::test] async fn retry_recovery_ensure_retries_exhausted(#[case] use_tower: bool) { - let clock = ClockControl::default().auto_advance_timers(true).to_clock(); + let clock = ClockControl::new_auto_advancing().to_clock(); let counter = Arc::new(AtomicU32::new(0)); let counter_clone = Arc::clone(&counter); @@ -152,7 +152,7 @@ async fn retry_recovery_ensure_retries_exhausted(#[case] use_tower: bool) { #[case::tower(true)] #[tokio::test] async fn retry_recovery_ensure_correct_delays(#[case] use_tower: bool) { - let clock = ClockControl::default().auto_advance_timers(true).to_clock(); + let clock = ClockControl::new_auto_advancing().to_clock(); let delays = Arc::new(Mutex::new(vec![])); let delays_clone = Arc::clone(&delays); @@ -191,7 +191,7 @@ async fn retry_recovery_ensure_correct_delays(#[case] use_tower: bool) { async fn retry_recovery_ensure_correct_attempts(#[case] use_tower: bool) { use seatbelt::Attempt; - let clock = ClockControl::default().auto_advance_timers(true).to_clock(); + let clock = ClockControl::new_auto_advancing().to_clock(); let attempts = Arc::new(Mutex::new(vec![])); let attempts_clone = Arc::clone(&attempts); @@ -243,7 +243,7 @@ async fn retry_recovery_ensure_correct_attempts(#[case] use_tower: bool) { #[case::tower(true)] #[tokio::test] async fn restore_input_integration_test(#[case] use_tower: bool) { - let clock = ClockControl::default().auto_advance_timers(true).to_clock(); + let clock = ClockControl::new_auto_advancing().to_clock(); let call_count = Arc::new(AtomicU32::new(0)); let call_count_clone = Arc::clone(&call_count); let restore_count = Arc::new(AtomicU32::new(0)); @@ -300,7 +300,7 @@ async fn restore_input_integration_test(#[case] use_tower: bool) { #[case::tower(true)] #[tokio::test] async fn outage_handling_disabled_no_retries(#[case] use_tower: bool) { - let clock = ClockControl::default().auto_advance_timers(true).to_clock(); + let clock = ClockControl::new_auto_advancing().to_clock(); let call_count = Arc::new(AtomicU32::new(0)); let call_count_clone = Arc::clone(&call_count); @@ -328,7 +328,7 @@ async fn outage_handling_disabled_no_retries(#[case] use_tower: bool) { #[case::tower(true)] #[tokio::test] async fn outage_handling_enabled_with_retries(#[case] use_tower: bool) { - let clock = ClockControl::default().auto_advance_timers(true).to_clock(); + let clock = ClockControl::new_auto_advancing().to_clock(); let call_count = Arc::new(AtomicU32::new(0)); let call_count_clone = Arc::clone(&call_count); @@ -365,7 +365,7 @@ async fn outage_handling_enabled_with_retries(#[case] use_tower: bool) { #[case::tower(true)] #[tokio::test] async fn outage_handling_with_recovery_hint(#[case] use_tower: bool) { - let clock = ClockControl::default().auto_advance_timers(true).to_clock(); + let clock = ClockControl::new_auto_advancing().to_clock(); let delays = Arc::new(Mutex::new(vec![])); let delays_clone = Arc::clone(&delays); @@ -400,7 +400,7 @@ async fn outage_handling_with_recovery_hint(#[case] use_tower: bool) { #[case::tower(true)] #[tokio::test] async fn clone_service_works_independently(#[case] use_tower: bool) { - let clock = ClockControl::default().auto_advance_timers(true).to_clock(); + let clock = ClockControl::new_auto_advancing().to_clock(); let call_count = Arc::new(AtomicU32::new(0)); let call_count_clone = Arc::clone(&call_count); diff --git a/crates/seatbelt/tests/timeout.rs b/crates/seatbelt/tests/timeout.rs index 23cd9b75b..c20042cff 100644 --- a/crates/seatbelt/tests/timeout.rs +++ b/crates/seatbelt/tests/timeout.rs @@ -63,9 +63,10 @@ async fn no_timeout(#[case] use_tower: bool) { #[case::tower(true)] #[tokio::test] async fn timeout(#[case] use_tower: bool) { - let clock = ClockControl::default() + let clock = ClockControl::builder() .auto_advance(Duration::from_millis(200)) .auto_advance_limit(Duration::from_millis(500)) + .build() .to_clock(); let context = ResilienceContext::new(clock.clone()); let called = Arc::new(AtomicBool::new(false)); @@ -101,9 +102,10 @@ async fn timeout(#[case] use_tower: bool) { #[case::tower(true)] #[tokio::test] async fn timeout_override_ensure_respected(#[case] use_tower: bool) { - let clock = ClockControl::default() + let clock = ClockControl::builder() .auto_advance(Duration::from_millis(200)) .auto_advance_limit(Duration::from_secs(5)) + .build() .to_clock(); let stack = ( @@ -140,7 +142,7 @@ async fn timeout_override_ensure_respected(#[case] use_tower: bool) { #[case::tower(true)] #[tokio::test] async fn no_timeout_if_disabled(#[case] use_tower: bool) { - let clock = ClockControl::default().auto_advance_timers(true).to_clock(); + let clock = ClockControl::new_auto_advancing().to_clock(); let stack = ( Timeout::layer("test_timeout", &ResilienceContext::new(&clock)) .timeout_output(|_args| Ok::<_, String>("timed out".to_string())) diff --git a/crates/seatbelt_http/src/breaker.rs b/crates/seatbelt_http/src/breaker.rs index a6ea1f20a..941920f6f 100644 --- a/crates/seatbelt_http/src/breaker.rs +++ b/crates/seatbelt_http/src/breaker.rs @@ -205,7 +205,7 @@ mod tests { #[test] fn server_errors_trip_breaker() { let handler = FakeHandler::from_fn(|_req| HttpResponseBuilder::new_fake().status(StatusCode::INTERNAL_SERVER_ERROR).build()); - let clock = ClockControl::default().auto_advance_timers(true).to_clock(); + let clock = ClockControl::new_auto_advancing().to_clock(); let context = crate::HttpResilienceContext::new(&clock); let service = ( @@ -233,7 +233,7 @@ mod tests { #[test] fn success_does_not_trip_breaker() { let handler = FakeHandler::from_fn(|_req| HttpResponseBuilder::new_fake().status(StatusCode::OK).build()); - let clock = ClockControl::default().auto_advance_timers(true).to_clock(); + let clock = ClockControl::new_auto_advancing().to_clock(); let context = crate::HttpResilienceContext::new(&clock); let service = ( @@ -260,7 +260,7 @@ mod tests { #[test] fn client_errors_do_not_trip_breaker() { let handler = FakeHandler::from_fn(|_req| HttpResponseBuilder::new_fake().status(StatusCode::BAD_REQUEST).build()); - let clock = ClockControl::default().auto_advance_timers(true).to_clock(); + let clock = ClockControl::new_auto_advancing().to_clock(); let context = crate::HttpResilienceContext::new(&clock); let service = ( diff --git a/crates/seatbelt_http/src/hedging.rs b/crates/seatbelt_http/src/hedging.rs index 2d3e6c600..3eaf75ebe 100644 --- a/crates/seatbelt_http/src/hedging.rs +++ b/crates/seatbelt_http/src/hedging.rs @@ -140,7 +140,7 @@ mod tests { #[test] fn hedging_recovers_with_safe_methods() { - let clock = ClockControl::default().auto_advance_timers(true).to_clock(); + let clock = ClockControl::new_auto_advancing().to_clock(); let context = crate::HttpResilienceContext::new(&clock); let service = ( @@ -157,7 +157,7 @@ mod tests { #[test] fn hedging_fails_with_unsafe_methods() { - let clock = ClockControl::default().auto_advance_timers(true).to_clock(); + let clock = ClockControl::new_auto_advancing().to_clock(); let context = crate::HttpResilienceContext::new(&clock); let service = ( @@ -182,7 +182,7 @@ mod tests { // reflects the routing decision produced by a custom `Router`. The // first attempt uses the original target, while hedged attempts must // be re-routed through the router before being dispatched. - let clock = ClockControl::default().auto_advance_timers(true).to_clock(); + let clock = ClockControl::new_auto_advancing().to_clock(); let context = crate::HttpResilienceContext::new(&clock); let captured_uris: Arc>> = Arc::new(Mutex::new(Vec::new())); diff --git a/crates/seatbelt_http/src/retry.rs b/crates/seatbelt_http/src/retry.rs index 040952be8..4cd09c4ec 100644 --- a/crates/seatbelt_http/src/retry.rs +++ b/crates/seatbelt_http/src/retry.rs @@ -168,7 +168,7 @@ mod tests { #[test] fn retry_recovers_with_safe_methods() { - let clock = ClockControl::default().auto_advance_timers(true).to_clock(); + let clock = ClockControl::new_auto_advancing().to_clock(); let context = crate::HttpResilienceContext::new(&clock); let service = ( @@ -185,7 +185,7 @@ mod tests { #[test] fn retry_fails_with_unsafe_methods() { - let clock = ClockControl::default().auto_advance_timers(true).to_clock(); + let clock = ClockControl::new_auto_advancing().to_clock(); let context = crate::HttpResilienceContext::new(&clock); let service = ( @@ -216,7 +216,7 @@ mod tests { HttpResponseBuilder::new_fake().status(StatusCode::OK).build() } }); - let clock = ClockControl::default().auto_advance_timers(true).to_clock(); + let clock = ClockControl::new_auto_advancing().to_clock(); let context = crate::HttpResilienceContext::new(&clock); let service = ( @@ -256,7 +256,7 @@ mod tests { // reflects the routing decision produced by a custom `Router`. The // first attempt uses the original target, while subsequent retry // attempts must be re-routed through the router before being dispatched. - let clock = ClockControl::default().auto_advance_timers(true).to_clock(); + let clock = ClockControl::new_auto_advancing().to_clock(); let context = crate::HttpResilienceContext::new(&clock); let captured_uris: Arc>> = Arc::new(Mutex::new(Vec::new())); diff --git a/crates/seatbelt_http/src/timeout.rs b/crates/seatbelt_http/src/timeout.rs index 702fec92d..098021932 100644 --- a/crates/seatbelt_http/src/timeout.rs +++ b/crates/seatbelt_http/src/timeout.rs @@ -87,7 +87,7 @@ mod tests { #[test] fn timeout_fires_on_slow_handler() { let handler = FakeHandler::never_completes(); - let clock = ClockControl::default().auto_advance_timers(true).to_clock(); + let clock = ClockControl::new_auto_advancing().to_clock(); let context = HttpResilienceContext::new(&clock); let service = ( @@ -107,7 +107,7 @@ mod tests { #[test] fn fast_handler_succeeds_within_timeout() { let handler = FakeHandler::from(StatusCode::OK); - let clock = ClockControl::default().auto_advance_timers(true).to_clock(); + let clock = ClockControl::new_auto_advancing().to_clock(); let context = HttpResilienceContext::new(&clock); let service = ( diff --git a/crates/tick/Cargo.toml b/crates/tick/Cargo.toml index 86130ab74..09bd7fab3 100644 --- a/crates/tick/Cargo.toml +++ b/crates/tick/Cargo.toml @@ -7,12 +7,13 @@ description = "Provides primitives to interact with and manipulate machine time. version = "0.4.0" readme = "README.md" keywords = ["time", "clock", "tick", "stopwatch"] -categories = ["data-structures"] +categories = ["date-and-time"] edition = { workspace = true } rust-version = { workspace = true } authors = { workspace = true } license = { workspace = true } homepage = { workspace = true } +documentation = "https://docs.rs/tick" include = { workspace = true } repository = "https://github.com/microsoft/oxidizer/tree/main/crates/tick" diff --git a/crates/tick/README.md b/crates/tick/README.md index 5b01fdf12..b816e6c68 100644 --- a/crates/tick/README.md +++ b/crates/tick/README.md @@ -46,7 +46,7 @@ mod tests { #[tokio::test] async fn test_produce_value() { // Automatically advance timers for instant, deterministic testing - let clock: Clock = ClockControl::new().auto_advance_timers(true).to_clock(); + let clock: Clock = ClockControl::new_auto_advancing().to_clock(); assert_eq!(produce_value(&clock).await, 123); } } @@ -141,20 +141,23 @@ examples for more details. ## Thread-aware relocation -All clock types implement [`ThreadAware`][__link31], supporting per-core -timer isolation in thread-per-core runtime architectures. +[`Clock`][__link31], [`SimpleClock`][__link32], [`ClockControl`][__link33], and +[`InactiveClock`][__link34] implement +[`ThreadAware`][__link35], supporting per-core timer isolation in +thread-per-core runtime architectures. [`InactiveClock`][__link36] +deliberately does not implement `ThreadAware` because one driver owns its shared timer set. -When an [`InactiveClock`][__link32] is -[relocated][__link33] to a target thread, the underlying timer -storage is duplicated per core. After activation, each thread’s [`Clock`][__link34] and -[`ClockDriver`][__link35] operate on an independent set of timers with no +When an [`InactiveClock`][__link37] is +[relocated][__link38] to a target thread, the underlying timer +storage is duplicated per core. After activation, each thread’s [`Clock`][__link39] and +[`ClockDriver`][__link40] operate on an independent set of timers with no cross-thread lock contention. -[`ClockControl`][__link36] clocks are unaffected by relocation, all clones always share the same +[`ClockControl`][__link41] clocks are unaffected by relocation, all clones always share the same controlled time state regardless of thread, so a single `ClockControl` can drive time for the entire test. -See the [`runtime`][__link37] module documentation for setup examples. +See the [`runtime`][__link42] module documentation for setup examples. ## Testing @@ -168,7 +171,7 @@ type, which is exposed when the `test-util` feature is enabled. ### Use `Clock` to retrieve absolute time -The clock provides absolute time as `SystemTime`. See [`Clock`][__link38] documentation for detailed +The clock provides absolute time as `SystemTime`. See [`Clock`][__link43] documentation for detailed information. ```rust @@ -187,7 +190,7 @@ assert!(time1 <= time2); ### Use `Clock` to retrieve relative time -The clock provides relative time via [`Clock::instant`][__link39] and [`Stopwatch`][__link40]. +The clock provides relative time via [`Clock::instant`][__link44] and [`Stopwatch`][__link45]. ```rust use std::time::{Duration, Instant}; @@ -242,18 +245,21 @@ timer This crate provides several optional features that can be enabled in your `Cargo.toml`: -* **`tokio`** - Integration with the [Tokio][__link41] runtime. Enables - [`Clock::new_tokio`][__link42] for creating clocks that use Tokio’s time facilities. -* **`test-util`** - Enables the [`ClockControl`][__link43] type for controlling the passage of time +* **`tokio`** - Integration with the [Tokio][__link46] runtime. Enables + [`Clock::new_tokio`][__link47] for creating clocks that use Tokio’s time facilities. +* **`test-util`** - Enables the [`ClockControl`][__link48] type for controlling the passage of time in tests. This allows you to pause time, advance it manually, or automatically advance timers for fast, deterministic testing. **Only enable this in `dev-dependencies`.** -* **`serde`** - Adds serialization and deserialization support via [serde][__link44]. -* **`fmt`** - Enables the [`fmt`][__link45] module with utilities for formatting `SystemTime` into +* **`serde`** - Adds serialization and deserialization support via [serde][__link49]. +* **`fmt`** - Enables the [`fmt`][__link50] module with utilities for formatting `SystemTime` into various formats (e.g., ISO 8601, RFC 2822). +* **`rt-shared`** - Enables + [`InactiveClock::new_shared`][__link51] + for runtimes that use one shared timer set and one driver. ## Additional Examples -The [time examples][__link46] +The [time examples][__link52] contain additional examples of how to use the time primitives. @@ -262,14 +268,14 @@ contain additional examples of how to use the time primitives. This crate was developed as part of The Oxidizer Project. Browse this crate's source code. - [__cargo_doc2readme_dependencies_info]: ggGmYW0CYXZlMC43LjJhdIQb11VxC_uAPOQbtUn4Wx2-BfAbid3Nt1Y27Pobprn8Z6FjFy9hYvRhcoQbn-ALXM8UiC8bIRESNiZavEAb64zavGelG-YbgLo76yq99ClhZIKCbHRocmVhZF9hd2FyZWUwLjguMIJkdGlja2UwLjQuMA - [__link0]: https://docs.rs/tick/0.4.0/tick/?search=ClockControl + [__cargo_doc2readme_dependencies_info]: ggGmYW0CYXZlMC43LjJhdIQb11VxC_uAPOQbtUn4Wx2-BfAbid3Nt1Y27Pobprn8Z6FjFy9hYvRhcoQb4vVdm_FO-OEbjxuBXAQgjTEbzSql2n-qk24bpqlkdn7_kcJhZIKCbHRocmVhZF9hd2FyZWUwLjguMIJkdGlja2UwLjQuMA + [__link0]: https://docs.rs/tick/latest/tick/struct.ClockControl.html [__link1]: https://docs.rs/tick/0.4.0/tick/?search=Clock [__link10]: https://docs.rs/tick/0.4.0/tick/?search=Error - [__link11]: https://docs.rs/tick/0.4.0/tick/fmt/index.html + [__link11]: https://docs.rs/tick/latest/tick/fmt/index.html [__link12]: https://docs.rs/tick/0.4.0/tick/runtime/index.html [__link13]: https://docs.rs/tick/0.4.0/tick/?search=FutureExt - [__link14]: https://docs.rs/tick/0.4.0/tick/?search=SystemTimeExt + [__link14]: https://docs.rs/tick/latest/tick/trait.SystemTimeExt.html [__link15]: https://doc.rust-lang.org/stable/std/?search=time::SystemTime [__link16]: https://docs.rs/tick/0.4.0/tick/?search=SimpleClock [__link17]: https://docs.rs/tick/0.4.0/tick/?search=Clock @@ -279,8 +285,8 @@ This crate was developed as part of Result<(), ohno::AppError> { let api = MyApi::new(&clock); // Execute some operation that uses the clock. - api.do_something().await; + api.do_something().await?; // Execute a periodic timer. let timer = PeriodicTimer::new(&clock, Duration::from_secs(2)); @@ -50,7 +50,7 @@ impl MyApi { Self { clock: clock.clone() } } - pub(crate) async fn do_something(&self) { + pub(crate) async fn do_something(&self) -> tick::Result<()> { // Start the measurement. let watch = self.clock.stopwatch(); @@ -60,7 +60,9 @@ impl MyApi { println!( "Work done. Elapsed: {}ms, Timestamp: {}", watch.elapsed().as_millis(), - self.clock.system_time_as::() + self.clock.try_system_time_as::()? ); + + Ok(()) } } diff --git a/crates/tick/examples/clock_control.rs b/crates/tick/examples/clock_control.rs index d879ea902..df4a27bde 100644 --- a/crates/tick/examples/clock_control.rs +++ b/crates/tick/examples/clock_control.rs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -#![expect(clippy::unwrap_used, reason = "example code")] - //! This example demonstrates how to use `ClockControl` to control the passage of time. use std::time::Duration; @@ -10,8 +8,8 @@ use std::time::Duration; use futures::executor::block_on; use tick::ClockControl; -fn main() { - let control = ClockControl::new().auto_advance_timers(true); +fn main() -> Result<(), std::time::SystemTimeError> { + let control = ClockControl::new_auto_advancing(); let clock = control.to_clock(); // Retrieve the current time. @@ -27,7 +25,7 @@ fn main() { control.advance(Duration::from_secs(1)); // Verify that time has advanced by 1 second. - assert_eq!(clock.system_time().duration_since(later).unwrap(), Duration::from_secs(1)); + assert_eq!(clock.system_time().duration_since(later)?, Duration::from_secs(1)); // Create a stopwatch. let stopwatch = clock.stopwatch(); @@ -43,4 +41,6 @@ fn main() { // because `auto_advance_timers` is set to true. // The delay finishes immediately. block_on(clock.delay(Duration::from_secs(1000))); + + Ok(()) } diff --git a/crates/tick/examples/data.rs b/crates/tick/examples/data.rs index df026120c..c196eeadd 100644 --- a/crates/tick/examples/data.rs +++ b/crates/tick/examples/data.rs @@ -23,12 +23,12 @@ async fn main() -> Result<(), ohno::AppError> { let mut cached_data: CachedData = serde_json::from_str(json)?; - cached_data.update(String::from("Hello, Rust!"), &clock); + cached_data.update(String::from("Hello, Rust!"), &clock)?; println!("Last access: {:?}", cached_data.last_access()); clock.delay(Duration::from_secs(1)).await; - cached_data.update(String::from("Hello again, Rust!"), &clock); + cached_data.update(String::from("Hello again, Rust!"), &clock)?; println!("Last access: {:?}", cached_data.last_access()); @@ -56,13 +56,16 @@ impl CachedData { const EXPIRATION: Duration = Duration::from_hours(1); /// Creates a new cached data instance with the current timestamp. - #[must_use] - pub fn new(id: u32, data: String, clock: &Clock) -> Self { - Self { + /// + /// # Errors + /// + /// Returns an error if the current system time cannot be represented as [`UnixSeconds`]. + pub fn new(id: u32, data: String, clock: &Clock) -> tick::Result { + Ok(Self { id, - last_access: clock.system_time_as::(), + last_access: clock.try_system_time_as::()?, data, - } + }) } /// Returns the timestamp when this data was last accessed. @@ -72,9 +75,14 @@ impl CachedData { } /// Updates the data and sets the last access time to the current timestamp. - pub fn update(&mut self, data: String, clock: &Clock) { + /// + /// # Errors + /// + /// Returns an error if the current system time cannot be represented as [`UnixSeconds`]. + pub fn update(&mut self, data: String, clock: &Clock) -> tick::Result<()> { self.data = data; - self.last_access = clock.system_time_as::(); + self.last_access = clock.try_system_time_as::()?; + Ok(()) } /// Checks if the cached data has expired based on the expiration duration. diff --git a/crates/tick/examples/interop_chrono.rs b/crates/tick/examples/interop_chrono.rs index f31c0a308..c702f379b 100644 --- a/crates/tick/examples/interop_chrono.rs +++ b/crates/tick/examples/interop_chrono.rs @@ -18,7 +18,7 @@ fn main() { let clock = Clock::new_frozen(); // Retrieve the current timestamp. - let timestamp = clock.system_time_as::>(); + let timestamp = DateTime::::from(clock.system_time()); println!("Current time (UTC): {}", timestamp.format(CHRONO_DISPLAY_FORMAT)); // Convert the timestamp to date time in Asia/Tokyo. We need to use diff --git a/crates/tick/examples/interop_jiff.rs b/crates/tick/examples/interop_jiff.rs index 02edb3cd7..f6773ba20 100644 --- a/crates/tick/examples/interop_jiff.rs +++ b/crates/tick/examples/interop_jiff.rs @@ -20,7 +20,7 @@ fn main() -> Result<(), ohno::AppError> { let clock = Clock::new_frozen(); // Retrieve the current time as `jiff::Timestamp`. - let timestamp = clock.system_time_as::(); + let timestamp = clock.try_system_time_as::()?; println!("Current time (UTC): {}", timestamp.strftime(JIFF_DISPLAY_FORMAT)); // Convert the timestamp to date time in Asia/Tokyo. diff --git a/crates/tick/examples/interop_time.rs b/crates/tick/examples/interop_time.rs index 23bc0f062..985e3df2d 100644 --- a/crates/tick/examples/interop_time.rs +++ b/crates/tick/examples/interop_time.rs @@ -17,7 +17,7 @@ fn main() -> Result<(), ohno::AppError> { let time_display_format = time::macros::format_description!("[year]-[month]-[day] [hour]:[minute]:[second]"); // Retrieve the current time. - let now = clock.system_time_as::(); + let now = OffsetDateTime::from(clock.system_time()); println!("Current time (UTC): {}", now.format(&time_display_format)?); Ok(()) diff --git a/crates/tick/src/clock.rs b/crates/tick/src/clock.rs index 1abbc21d4..a724c30fb 100644 --- a/crates/tick/src/clock.rs +++ b/crates/tick/src/clock.rs @@ -43,10 +43,11 @@ use crate::timers::TimerKey; /// # Clock construction /// /// The clock requires a runtime to drive the registered timers. This crate provides built-in support -/// for Tokio via [`Clock::new_tokio()`] (available with the `tokio` feature). For other async runtimes, +/// for Tokio via [`Clock::new_tokio`] (available with the `tokio` feature). For other async runtimes, /// you can use types in the [`runtime`][crate::runtime] module to drive the clock. /// -/// In tests, the clock can be constructed directly using [`ClockControl`][crate::ClockControl] or via [`Clock::new_frozen`][crate::Clock::new_frozen] +/// In tests, the clock can be constructed directly using [`ClockControl`] or via +/// [`Clock::new_frozen`](https://docs.rs/tick/latest/tick/struct.Clock.html#method.new_frozen) /// (available with the `test-util` feature) because the passage of time is controlled manually. /// /// See the [Testing](#testing) section for more information. @@ -59,7 +60,7 @@ use crate::timers::TimerKey; /// /// The ability to jump forward in time makes tests faster, more reliable and gives you complete control over the passage of time. /// By default, the clock does not allow you to control the passage of time. However, when the `test-util` feature is enabled, -/// this crate provides a [`ClockControl`][crate::ClockControl] type that can be used to control time. +/// this crate provides a [`ClockControl`] type that can be used to control time. /// /// # Cloning and shared state /// @@ -93,10 +94,10 @@ use crate::timers::TimerKey; /// /// - **`ClockControl` clocks** (`test-util`): Relocation is a no-op. All clones share the same /// controlled time state regardless of which thread they are on. This is intentional, a single -/// [`ClockControl`][crate::ClockControl] controls time for all clocks derived from it, even +/// [`ClockControl`] controls time for all clocks derived from it, even /// across threads. /// -/// - **Tokio clocks** (created via [`Clock::new_tokio()`]): Relocation is a no-op. The Tokio clock +/// - **Tokio clocks** (created via [`Clock::new_tokio`]): Relocation is a no-op. The Tokio clock /// is driven by a single background task that advances a shared set of timers, so all clones /// share the same timer storage regardless of which thread they are on. Per-core relocation /// would create independent timer storage on the destination thread that the background driver @@ -172,6 +173,9 @@ use crate::timers::TimerKey; /// } /// # } /// ``` +/// +/// [`ClockControl`]: https://docs.rs/tick/latest/tick/struct.ClockControl.html +/// [`Clock::new_tokio`]: https://docs.rs/tick/latest/tick/struct.Clock.html#method.new_tokio #[derive(Clone)] pub struct Clock { state: ClockState, @@ -339,7 +343,7 @@ impl Clock { self.simple_clock().system_time() } - /// Retrieves the current system time converted to a target type. + /// Retrieves the current system time and converts it to `T`. /// /// This is a convenience method that retrieves the current [`SystemTime`] via /// [`system_time()`][Self::system_time] and converts it to the specified target type. @@ -349,26 +353,26 @@ impl Clock { /// * `T` - The target type that implements [`TryFrom`]. Common examples include /// timestamp types from external crates that can be constructed from a [`SystemTime`]. /// - /// # Panics + /// # Errors /// - /// Panics if the current system time cannot be represented by the target type `T`. + /// Returns the target type's conversion error when the current [`SystemTime`] is outside + /// the target type's representable range. /// - /// Callers must choose a target type whose representable range covers the - /// [`SystemTime`] values they expect. The conversion can fail in two cases: + /// # Examples /// - /// - **In production**, if `T` has a narrower representable range than [`SystemTime`] and the - /// current system time falls outside it. - /// - **In tests** using manual time control (via the `test-util` feature), if controlled time - /// is moved outside the target type's supported range. + /// ``` + /// use std::time::SystemTime; /// - /// If `T` can represent every [`SystemTime`] value, this conversion never fails and this method - /// never panics — the panic above only occurs for target types whose representable range does - /// not cover the current time. + /// use tick::Clock; /// - /// Kept consistent with [`SimpleClock::system_time_as`][crate::SimpleClock::system_time_as]. - #[must_use] - pub fn system_time_as>(&self) -> T { - self.simple_clock().system_time_as() + /// # fn example(clock: &Clock) -> Result<(), std::convert::Infallible> { + /// let time = clock.try_system_time_as::()?; + /// assert_eq!(time, clock.system_time()); + /// # Ok(()) + /// # } + /// ``` + pub fn try_system_time_as>(&self) -> Result { + self.simple_clock().try_system_time_as() } /// Retrieves the current [`Instant`] time. @@ -425,7 +429,7 @@ impl Clock { /// assert!(stopwatch.elapsed() >= Duration::from_millis(10)); /// # } /// ``` - #[must_use] + #[must_use = "futures do nothing unless awaited or polled"] pub fn delay(&self, duration: Duration) -> crate::Delay { crate::Delay::new(self, duration) } @@ -471,6 +475,14 @@ impl Clock { } } + pub(super) fn update_timer_waker(&self, key: TimerKey, waker: &Waker) { + match self.clock_state() { + #[cfg(any(feature = "test-util", test))] + ClockState::ClockControl(control) => control.update_timer_waker(key, waker), + ClockState::System(timers) => timers.with_timers(|timers| timers.update_waker(key, waker)), + } + } + pub(crate) fn clock_state(&self) -> &ClockState { &self.state } @@ -656,8 +668,7 @@ mod tests { } #[test] - #[should_panic(expected = "target type cannot represent the current SystemTime")] - fn system_time_as_panics_on_conversion_failure() { + fn try_system_time_as_returns_conversion_failure() { /// A newtype that always fails conversion from `SystemTime`. struct AlwaysFailsConversion; @@ -670,7 +681,10 @@ mod tests { } let clock = Clock::new_frozen(); - let _: AlwaysFailsConversion = clock.system_time_as(); + assert_eq!( + clock.try_system_time_as::().err(), + Some("conversion always fails") + ); } #[test] diff --git a/crates/tick/src/clock_control.rs b/crates/tick/src/clock_control.rs index b69aefbe3..5dd652ee5 100644 --- a/crates/tick/src/clock_control.rs +++ b/crates/tick/src/clock_control.rs @@ -6,7 +6,7 @@ use std::task::Waker; use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; use crate::state::ClockState; -use crate::timers::{TimerKey, Timers}; +use crate::timers::{ReadyTimers, TimerKey, Timers}; use crate::{Clock, thread_aware_move}; /// Controls the passage of time in tests. @@ -42,8 +42,9 @@ use crate::{Clock, thread_aware_move}; /// ``` /// # use std::time::Duration; /// # use tick::{Clock, ClockControl}; -/// let clock = ClockControl::new() +/// let clock = ClockControl::builder() /// .auto_advance(Duration::from_secs(1)) +/// .build() /// .to_clock(); /// /// let now = clock.system_time(); @@ -92,6 +93,37 @@ impl std::fmt::Debug for ClockControl { thread_aware_move!(ClockControl); +/// Configures and creates a [`ClockControl`]. +/// +/// Use [`ClockControl::builder`] when controlled time needs automatic advancement, +/// an advancement limit, or a non-default initial time. +/// +/// # Examples +/// +/// ``` +/// use std::time::{Duration, SystemTime}; +/// +/// use tick::ClockControl; +/// +/// let control = ClockControl::builder() +/// .time(SystemTime::UNIX_EPOCH + Duration::from_secs(10)) +/// .auto_advance(Duration::from_secs(1)) +/// .build(); +/// +/// let clock = control.to_clock(); +/// assert_eq!( +/// clock.system_time(), +/// SystemTime::UNIX_EPOCH + Duration::from_secs(10) +/// ); +/// ``` +#[derive(Debug, Clone)] +pub struct ClockControlBuilder { + time: SystemTime, + auto_advance: Duration, + auto_advance_total_max: Option, + auto_advance_timers: bool, +} + impl ClockControl { /// Creates a new `ClockControl` instance. /// @@ -103,8 +135,9 @@ impl ClockControl { /// /// use tick::ClockControl; /// - /// let clock = ClockControl::new() + /// let clock = ClockControl::builder() /// .auto_advance(Duration::from_secs(1)) + /// .build() /// .to_clock(); /// /// let time1 = clock.system_time(); @@ -116,11 +149,29 @@ impl ClockControl { /// ``` #[must_use] pub fn new() -> Self { - Self { - state: Arc::new(Mutex::new(State::default())), + Self::default() + } + + /// Creates a builder for a configured `ClockControl`. + #[must_use] + pub fn builder() -> ClockControlBuilder { + ClockControlBuilder { + time: SystemTime::UNIX_EPOCH, + auto_advance: Duration::ZERO, + auto_advance_total_max: None, + auto_advance_timers: false, } } + /// Creates a clock control that automatically advances pending timers. + /// + /// This is a shortcut for + /// `ClockControl::builder().auto_advance_timers().build()`. + #[must_use] + pub fn new_auto_advancing() -> Self { + Self::builder().auto_advance_timers().build() + } + /// Creates a new `ClockControl` instance at the specified time. /// /// # Examples @@ -138,9 +189,7 @@ impl ClockControl { /// ``` #[must_use] pub fn new_at(time: impl Into) -> Self { - let this = Self::new(); - this.set_time(time.into()); - this + Self::builder().time(time).build() } /// Converts this `ClockControl` into a `Clock` instance. @@ -151,6 +200,8 @@ impl ClockControl { /// # Examples /// /// ``` + /// use std::time::Duration; + /// /// use tick::ClockControl; /// /// let control = ClockControl::new(); @@ -158,7 +209,7 @@ impl ClockControl { /// let clock_clone = clock.clone(); /// /// // Advance the clock by 1 second - /// control.advance_millis(1_000); + /// control.advance(Duration::from_secs(1)); /// /// // Ensure the clock and cloned clock are in sync /// assert_eq!(clock.system_time(), clock_clone.system_time()); @@ -197,153 +248,6 @@ impl ClockControl { crate::SimpleClock::from_control(self.clone()) } - /// Sets the duration by which the clock will auto-advance when accessing the current time. - /// - /// # Examples - /// - /// ``` - /// use std::time::Duration; - /// - /// use tick::ClockControl; - /// - /// let clock = ClockControl::new() - /// .auto_advance(Duration::from_secs(1)) - /// .to_clock(); - /// - /// let now = clock.system_time(); - /// let later = clock.system_time(); // Automatically advances by 1 second - /// - /// assert_eq!(later.duration_since(now)?, Duration::from_secs(1)); - /// - /// # Ok::<(), Box>(()) - /// ``` - #[must_use] - pub fn auto_advance(self, duration: Duration) -> Self { - self.with_state(|v| v.auto_advance = duration); - self - } - - /// Sets a limit on the total auto-advance duration. - /// - /// When auto-advance is enabled via [`Self::auto_advance`], this method limits the total - /// amount of time that can be auto-advanced. Once the limit is reached, further calls to - /// access the current time will no longer auto-advance the clock. - /// - /// > **Note**: This method only has an effect if [`Self::auto_advance`] has been called - /// > previously to set a non-zero auto-advance duration. - /// - /// # Examples - /// - /// ``` - /// use std::time::Duration; - /// - /// use tick::{ClockControl, FutureExt}; - /// - /// # async fn auto_advance_limit_example() { - /// // Limit the max auto-advance to 500ms. The 700ms delay never completes because - /// // the total auto-advance is capped. Instead, the 200ms timeout completes. - /// let clock = ClockControl::new() - /// .auto_advance(Duration::from_millis(200)) - /// .auto_advance_limit(Duration::from_millis(500)) - /// .to_clock(); - /// - /// // Create a long-running future and apply a timeout - /// let timeout_error = clock - /// .delay(Duration::from_millis(700)) - /// .timeout(&clock, Duration::from_millis(200)) - /// .await - /// .unwrap_err(); - /// - /// assert_eq!(timeout_error.to_string(), "future timed out"); - /// # } - /// ``` - #[must_use] - pub fn auto_advance_limit(self, limit: Duration) -> Self { - self.with_state(|v| { - v.auto_advance_total_max = Some(limit); - }); - - self - } - - /// Configures whether the clock automatically advances to fire pending timers. - /// - /// When enabled, the clock fast-forwards on its own to fire timers, so timer-based futures - /// such as [`Delay`](crate::Delay) complete without any manual clock advancement and without - /// waiting for real time to elapse. This is the simplest way to keep time-dependent tests both - /// deterministic and fast: awaiting a delay resolves right away in wall-clock terms. - /// - /// The clock advances both when a timer is scheduled and when the current time is read, jumping - /// forward to a timer's deadline before firing it. Simulated time is still consumed, so - /// elapsed-time measurements (for example via a [`Stopwatch`](crate::Stopwatch)) reflect each - /// delay's configured duration. A delay with no finite deadline - /// ([`Duration::MAX`](std::time::Duration::MAX)) never completes. - /// - /// # Timers do not run concurrently - /// - /// Timers are fired eagerly, one at a time, as they are scheduled; this option does not - /// simulate multiple timers running side by side. Do not rely on concurrent delays completing - /// in duration order — a shorter delay scheduled after a longer one may still complete after - /// it. When the relative ordering of concurrent timers matters, drive the clock explicitly with - /// [`Self::advance`] instead. - /// - /// > **Note**: When [`Self::auto_advance_limit`] is set, the maximum total auto-advance - /// > duration is respected. Once the limit is reached, no further timers will be fired - /// > automatically. - /// - /// # Examples - /// - /// ``` - /// use std::time::Duration; - /// - /// use tick::ClockControl; - /// - /// # async fn auto_advance_timers_example() { - /// let clock = ClockControl::new().auto_advance_timers(true).to_clock(); - /// - /// let stopwatch = clock.stopwatch(); - /// - /// // Resolves right away in real time — no manual `advance` call is needed. - /// clock.delay(Duration::from_secs(30)).await; - /// - /// // Simulated time still elapses by the delay's duration. - /// assert_eq!(stopwatch.elapsed(), Duration::from_secs(30)); - /// # } - /// ``` - #[must_use] - pub fn auto_advance_timers(self, enabled: bool) -> Self { - self.with_state(|v| v.auto_advance_timers = enabled); - self - } - - /// Manually advances the clock by the specified number of milliseconds. - /// - /// In addition to advancing the current time, this method fires any registered timers - /// that are scheduled to expire within the advanced period. - /// - /// # Examples - /// - /// ``` - /// use std::time::Duration; - /// - /// use tick::ClockControl; - /// - /// let control = ClockControl::new(); - /// let clock = control.to_clock(); - /// - /// let now = clock.system_time(); - /// control.advance_millis(100); - /// assert_eq!( - /// clock.system_time().duration_since(now)?, - /// Duration::from_millis(100) - /// ); - /// - /// # Ok::<(), Box>(()) - /// ``` - pub fn advance_millis(&self, millis: u64) { - self.advance(Duration::from_millis(millis)); - } - /// Manually advances the clock by the specified duration. /// /// In addition to advancing the current time, this method fires any registered timers @@ -368,8 +272,13 @@ impl ClockControl { /// /// # Ok::<(), Box>(()) /// ``` + /// + /// # Panics + /// + /// Panics if the duration would move the controlled [`SystemTime`] or [`Instant`] outside + /// the range supported by the platform. pub fn advance(&self, duration: Duration) { - self.with_state(|v| v.advance(duration, TimeFlow::Forward)); + self.with_state_and_wake(|state, ready| state.advance(duration, TimeFlow::Forward, ready)); } /// Sets the clock to the specified system time. @@ -389,38 +298,33 @@ impl ClockControl { /// /// assert_eq!(clock.system_time(), target); /// ``` - #[expect( - clippy::missing_panics_doc, - reason = "we are handling cases where the timestamp is either in future or past and the resulting duration is always positive" - )] + /// + /// # Panics + /// + /// Panics if `timestamp` would move the controlled [`SystemTime`] or [`Instant`] outside + /// the range supported by the platform. pub fn set_time(&self, timestamp: impl Into) { - let now = self.system_time(); - let timestamp = timestamp.into(); - - match timestamp.duration_since(now) { - Ok(duration) => { - self.with_state(|v| v.advance(duration, TimeFlow::Forward)); - } - Err(_e) => { - let duration = now.duration_since(timestamp).expect("the resulting duration must be positive here"); - - self.with_state(|v| v.advance(duration, TimeFlow::Backward)); - } - } + self.with_state_and_wake(|state, ready| state.set_time(timestamp.into(), ready)); } pub(super) fn system_time(&self) -> SystemTime { - self.with_state(State::now) + self.with_state_and_wake(State::now) } pub(super) fn instant(&self) -> Instant { - self.with_state(State::instant_now) + self.with_state_and_wake(State::instant_now) } pub(super) fn register_timer(&self, when: Instant, waker: Waker) -> TimerKey { - let key = self.with_state(|s| s.timers.register(when, waker)); - self.with_state(State::evaluate_timers); - key + self.with_state_and_wake(|state, ready| { + let key = state.timers.register(when, waker); + state.evaluate_timers(ready); + key + }) + } + + pub(super) fn update_timer_waker(&self, key: TimerKey, waker: &Waker) { + self.with_state(|state| state.timers.update_waker(key, waker)); } pub(super) fn unregister_timer(&self, key: TimerKey) { @@ -439,7 +343,22 @@ impl ClockControl { where F: FnOnce(&mut State) -> R, { - f(&mut self.state.lock().expect("acquiring lock must always succeed")) + f(&mut self.state.lock().expect("clock control lock poisoned")) + } + + fn with_state_and_wake(&self, f: F) -> R + where + F: FnOnce(&mut State, &mut ReadyTimers) -> R, + { + let mut ready = ReadyTimers::new(); + let result = self.with_state(|state| f(state, &mut ready)); + + // A wake may synchronously re-enter clock control, so invoke it after releasing the lock. + for waker in ready.into_values() { + waker.wake(); + } + + result } pub(crate) fn is_unique(&self) -> bool { @@ -447,6 +366,64 @@ impl ClockControl { } } +impl ClockControlBuilder { + /// Sets the initial system time. + /// + /// The default is [`SystemTime::UNIX_EPOCH`]. + #[must_use] + pub fn time(mut self, time: impl Into) -> Self { + self.time = time.into(); + self + } + + /// Sets the duration advanced whenever the current time is read. + #[must_use] + pub fn auto_advance(mut self, duration: Duration) -> Self { + self.auto_advance = duration; + self + } + + /// Limits the total duration consumed by automatic advancement. + /// + /// The limit applies to both read-based auto-advance and timer auto-advance. + #[must_use] + pub fn auto_advance_limit(mut self, limit: Duration) -> Self { + self.auto_advance_total_max = Some(limit); + self + } + + /// Enables automatic advancement to pending timer deadlines. + /// + /// Timers are fired eagerly, one at a time, as they are scheduled. This does not simulate + /// concurrent timers; use [`ClockControl::advance`] when timer ordering matters. + #[must_use] + pub fn auto_advance_timers(mut self) -> Self { + self.auto_advance_timers = true; + self + } + + /// Builds the configured [`ClockControl`]. + /// + /// # Panics + /// + /// Panics if the initial time is outside the range supported by the platform's + /// [`SystemTime`] or [`Instant`]. + #[must_use] + pub fn build(self) -> ClockControl { + let mut state = State::default(); + let mut ready = ReadyTimers::new(); + state.set_time(self.time, &mut ready); + debug_assert!(ready.is_empty(), "a newly created clock has no timers to wake"); + state.auto_advance = self.auto_advance; + state.auto_advance_total_max = self.auto_advance_total_max; + state.auto_advance_timers = self.auto_advance_timers; + + ClockControl { + state: Arc::new(Mutex::new(state)), + } + } +} + impl From for Clock { fn from(control: ClockControl) -> Self { control.to_clock() @@ -497,10 +474,17 @@ impl Default for State { } impl State { - fn auto_advance(&mut self, duration: Option) { + fn set_time(&mut self, timestamp: SystemTime, ready: &mut ReadyTimers) { + match timestamp.duration_since(self.system_time) { + Ok(duration) => self.advance(duration, TimeFlow::Forward, ready), + Err(error) => self.advance(error.duration(), TimeFlow::Backward, ready), + } + } + + fn auto_advance(&mut self, duration: Option, ready: &mut ReadyTimers) { let auto_advance = self.get_next_auto_advance_duration(duration.unwrap_or(self.auto_advance)); self.auto_advance_total = self.auto_advance_total.saturating_add(auto_advance); - self.advance(auto_advance, TimeFlow::Forward); + self.advance(auto_advance, TimeFlow::Forward, ready); } fn get_next_auto_advance_duration(&self, hint: Duration) -> Duration { @@ -513,13 +497,13 @@ impl State { } #[cfg_attr(test, mutants::skip)] // causes test timeout - fn advance(&mut self, duration: Duration, flow: TimeFlow) { + fn advance(&mut self, duration: Duration, flow: TimeFlow, ready: &mut ReadyTimers) { self.advance_time(duration, flow); - self.evaluate_timers(); + self.evaluate_timers(ready); } - fn evaluate_timers(&mut self) { - self.timers.advance_timers(self.instant); + fn evaluate_timers(&mut self, ready: &mut ReadyTimers) { + self.timers.advance_timers(self.instant, ready); if !self.auto_advance_timers { return; @@ -538,8 +522,14 @@ impl State { break; } - self.advance(advance, TimeFlow::Forward); + let previous_instant = self.instant; + self.advance_time(advance, TimeFlow::Forward); + assert!( + self.instant > previous_instant, + "positive forward advancement must move the clock instant" + ); self.auto_advance_total = self.auto_advance_total.saturating_add(advance); + self.timers.advance_timers(self.instant, ready); } } @@ -552,28 +542,23 @@ impl State { TimeFlow::Forward => { self.instant = self.instant.checked_add(duration).expect(OUTSIDE_RANGE_MESSAGE); self.system_time = self.system_time.checked_add(duration).expect(OUTSIDE_RANGE_MESSAGE); - self.timers.advance_timers(self.instant); } TimeFlow::Backward => { self.instant = self.instant.checked_sub(duration).expect(OUTSIDE_RANGE_MESSAGE); self.system_time = self.system_time.checked_sub(duration).expect(OUTSIDE_RANGE_MESSAGE); - - // There is no point in advancing/triggering the timers if we are moving back - // in time. Timers are only ever fired when time moves forward. - // No need to call `self.timers.advance_timers` here. } } } - fn now(&mut self) -> SystemTime { + fn now(&mut self, ready: &mut ReadyTimers) -> SystemTime { let time = self.system_time; - self.auto_advance(None); + self.auto_advance(None, ready); time } - fn instant_now(&mut self) -> Instant { + fn instant_now(&mut self, ready: &mut ReadyTimers) -> Instant { let time = self.instant; - self.auto_advance(None); + self.auto_advance(None, ready); time } } @@ -590,12 +575,15 @@ static OUTSIDE_RANGE_MESSAGE: &str = #[cfg_attr(coverage_nightly, coverage(off))] #[cfg(test)] mod tests { + use std::sync::atomic::{AtomicBool, Ordering}; + use super::*; use crate::fmt::UnixSeconds; #[test] fn assert_types() { - static_assertions::assert_impl_all!(ClockControl: Send, Sync); + static_assertions::assert_impl_all!(ClockControl: Send, Sync, Clone, Default); + static_assertions::assert_impl_all!(ClockControlBuilder: Send, Sync, Clone); } #[test] @@ -608,10 +596,18 @@ mod tests { assert_eq!(control.system_time(), SystemTime::UNIX_EPOCH); } + #[test] + fn builder_defaults_ok() { + let control = ClockControl::builder().build(); + + assert_eq!(control.with_state(|s| s.auto_advance), Duration::ZERO); + assert_eq!(control.system_time(), SystemTime::UNIX_EPOCH); + } + #[test] fn auto_advance_ok() { let duration = Duration::from_secs(1); - let control = ClockControl::new().auto_advance(duration); + let control = ClockControl::builder().auto_advance(duration).build(); let clock = control.to_clock(); assert_eq!(control.with_state(|s| s.auto_advance), duration); @@ -672,20 +668,6 @@ mod tests { assert_eq!(instant_now1.checked_duration_since(instant_now2).unwrap(), Duration::from_secs(5)); } - #[test] - fn advance_millis_ok() { - // arrange - let control = ClockControl::new(); - let clock = control.to_clock(); - let now = clock.system_time(); - - // act - () = control.advance_millis(123); - - // assert - assert_eq!(clock.system_time().duration_since(now).unwrap(), Duration::from_millis(123)); - } - #[test] fn register_timer_ok() { // arrange @@ -725,7 +707,7 @@ mod tests { #[test] fn auto_advance_timers() { - let control = ClockControl::new().auto_advance_timers(true); + let control = ClockControl::new_auto_advancing(); let clock = control.to_clock(); let now = clock.system_time(); @@ -749,11 +731,28 @@ mod tests { assert_eq!(control.timers_len(), 0); } + #[test] + fn timer_wakers_run_after_releasing_state_lock() { + let control = ClockControl::new(); + let clock = control.to_clock(); + let lock_was_available = Arc::new(AtomicBool::new(false)); + let waker = Waker::from(Arc::new(ReentrantWaker { + control: control.clone(), + lock_was_available: Arc::clone(&lock_was_available), + })); + + control.register_timer(clock.instant() + Duration::from_secs(1), waker); + control.advance(Duration::from_secs(1)); + + assert!(lock_was_available.load(Ordering::Relaxed)); + } + #[test] fn auto_advance_limit() { - let control = ClockControl::new() + let control = ClockControl::builder() .auto_advance(Duration::from_millis(550)) - .auto_advance_limit(Duration::from_secs(2)); + .auto_advance_limit(Duration::from_secs(2)) + .build(); let clock = control.to_clock(); let anchor = clock.system_time(); @@ -794,7 +793,7 @@ mod tests { // Before the fix, this would overflow because: // evaluate_timers -> advance_time -> evaluate_timers -> advance_time -> ... - let control = ClockControl::new().auto_advance_timers(true); + let control = ClockControl::new_auto_advancing(); let clock = control.to_clock(); let start_instant = clock.instant(); @@ -817,7 +816,7 @@ mod tests { // iteratively without stack overflow. The loop-based implementation prevents // recursion: evaluate_timers -> advance_time -> timers.advance_timers (not evaluate_timers again) - let control = ClockControl::new().auto_advance_timers(true); + let control = ClockControl::new_auto_advancing(); let clock = control.to_clock(); let start_instant = clock.instant(); @@ -839,7 +838,7 @@ mod tests { #[test] fn from_clock_control_ok() { let control = ClockControl::default(); - control.advance_millis(12345); + control.advance(Duration::from_millis(12345)); let clock_1 = Clock::from(control.clone()); let clock_2 = Clock::from(&control); @@ -850,10 +849,11 @@ mod tests { #[test] fn auto_advance_timers_stops_at_limit() { - let control = ClockControl::new() - .auto_advance_timers(true) + let control = ClockControl::builder() + .auto_advance_timers() .auto_advance(Duration::from_secs(1)) - .auto_advance_limit(Duration::from_secs(1)); + .auto_advance_limit(Duration::from_secs(1)) + .build(); let clock = control.to_clock(); let start_instant = clock.instant(); @@ -891,4 +891,16 @@ mod tests { insta::assert_debug_snapshot!(control); } + + struct ReentrantWaker { + control: ClockControl, + lock_was_available: Arc, + } + + impl std::task::Wake for ReentrantWaker { + fn wake(self: Arc) { + self.lock_was_available + .store(self.control.state.try_lock().is_ok(), Ordering::Relaxed); + } + } } diff --git a/crates/tick/src/delay.rs b/crates/tick/src/delay.rs index ce9255eb8..72c3e9ad6 100644 --- a/crates/tick/src/delay.rs +++ b/crates/tick/src/delay.rs @@ -36,6 +36,7 @@ use super::timers::TimerKey; /// # } /// ``` #[derive(Debug)] +#[must_use = "futures do nothing unless awaited or polled"] pub struct Delay { // Currently scheduled timer. This value is not initialized before // actually calling the `Future::poll` method. @@ -68,7 +69,7 @@ impl Delay { /// assert!(stopwatch.elapsed() >= Duration::from_millis(10)); /// # } /// ``` - #[must_use] + #[must_use = "futures do nothing unless awaited or polled"] pub fn new(clock: &Clock, duration: Duration) -> Self { Self { duration, @@ -112,7 +113,10 @@ impl Future for Delay { Poll::Ready(()) } - Some(_) => Poll::Pending, + Some(key) => { + this.clock.update_timer_waker(key, cx.waker()); + Poll::Pending + } } } } @@ -128,6 +132,9 @@ impl Drop for Delay { #[cfg_attr(coverage_nightly, coverage(off))] #[cfg(test)] mod tests { + use std::sync::Arc; + use std::sync::atomic::{AtomicUsize, Ordering}; + use std::task::Wake; use std::thread; use super::*; @@ -232,10 +239,42 @@ mod tests { assert_eq!(clock.clock_state().timers_len(), 0); } + #[test] + fn latest_waker_is_notified() { + let control = ClockControl::new(); + let clock = control.to_clock(); + let mut delay = Delay::new(&clock, Duration::from_secs(1)); + let first = Arc::new(WakeCounter::default()); + let second = Arc::new(WakeCounter::default()); + + assert_eq!(poll_delay_with_waker(&mut delay, &Waker::from(Arc::clone(&first))), Poll::Pending); + assert_eq!(poll_delay_with_waker(&mut delay, &Waker::from(Arc::clone(&second))), Poll::Pending); + + control.advance(Duration::from_secs(1)); + + assert_eq!(first.count.load(Ordering::Relaxed), 0); + assert_eq!(second.count.load(Ordering::Relaxed), 1); + } + fn poll_delay(delay: &mut Delay) -> Poll<()> { - let mut cx = Context::from_waker(Waker::noop()); + poll_delay_with_waker(delay, Waker::noop()) + } + + fn poll_delay_with_waker(delay: &mut Delay, waker: &Waker) -> Poll<()> { + let mut cx = Context::from_waker(waker); let delay = std::pin::pin!(delay); delay.poll(&mut cx) } + + #[derive(Default)] + struct WakeCounter { + count: AtomicUsize, + } + + impl Wake for WakeCounter { + fn wake(self: Arc) { + self.count.fetch_add(1, Ordering::Relaxed); + } + } } diff --git a/crates/tick/src/error.rs b/crates/tick/src/error.rs index 823eb07d7..22c2761af 100644 --- a/crates/tick/src/error.rs +++ b/crates/tick/src/error.rs @@ -1,10 +1,22 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +use std::backtrace::{Backtrace, BacktraceStatus}; use std::fmt; use std::time::SystemTimeError; -/// The result type for fallible operations that use the [`Error`] type in the `time` module. +/// The result type for fallible operations in this crate. +/// +/// # Examples +/// +/// ``` +/// fn operation() -> tick::Result<()> { +/// Ok(()) +/// } +/// +/// operation()?; +/// # Ok::<(), tick::Error>(()) +/// ``` pub type Result = std::result::Result; /// An error that can occur in the `tick` crate. @@ -15,24 +27,52 @@ pub type Result = std::result::Result; /// * Parsing and formatting errors. /// * Validation problems. /// -/// # Limited introspection -/// -/// Other than implementing the [`std::error::Error`] and [`core::fmt::Debug`] traits, this error type -/// currently provides no introspection capabilities. -/// /// # Examples /// /// ``` +/// # #[cfg(feature = "fmt")] +/// # { /// use tick::Error; /// use tick::fmt::Iso8601; /// -/// "invalid date".parse::().unwrap_err(); +/// let result: Result = "invalid date".parse(); +/// assert!(matches!(result, Err(error) if !error.is_timeout())); +/// # } /// ``` #[derive(Debug)] -pub struct Error(ErrorKind); +pub struct Error { + kind: ErrorKind, + backtrace: MaybeBacktrace, +} crate::thread_aware_move!(Error); +#[derive(Debug)] +enum MaybeBacktrace { + Captured(Box), + Disabled, +} + +impl MaybeBacktrace { + fn capture() -> Self { + Self::from_backtrace(Backtrace::capture()) + } + + fn from_backtrace(backtrace: Backtrace) -> Self { + match backtrace.status() { + BacktraceStatus::Captured => Self::Captured(Box::new(backtrace)), + _ => Self::Disabled, + } + } + + const fn get(&self) -> Option<&Backtrace> { + match self { + Self::Captured(backtrace) => Some(backtrace), + Self::Disabled => None, + } + } +} + #[derive(Debug)] enum ErrorKind { #[cfg(any(feature = "fmt", test))] @@ -41,11 +81,15 @@ enum ErrorKind { OutOfRange(std::borrow::Cow<'static, str>), Other(Box), SystemTimeError(SystemTimeError), + Timeout, } impl Error { - const fn from_kind(kind: ErrorKind) -> Self { - Self(kind) + fn from_kind(kind: ErrorKind) -> Self { + Self { + kind, + backtrace: MaybeBacktrace::capture(), + } } #[cfg(any(feature = "fmt", test))] @@ -54,7 +98,7 @@ impl Error { } #[cfg(any(feature = "fmt", test))] - pub(super) const fn jiff(error: jiff::Error) -> Self { + pub(super) fn jiff(error: jiff::Error) -> Self { Self::from_kind(ErrorKind::Jiff(error)) } @@ -62,41 +106,103 @@ impl Error { Self::from_kind(ErrorKind::Other(Box::new(error))) } + pub(super) fn timeout() -> Self { + Self::from_kind(ErrorKind::Timeout) + } + + /// Returns whether this error reports a future timeout. + /// + /// # Examples + /// + /// ``` + /// # #[cfg(feature = "test-util")] + /// # { + /// use std::time::Duration; + /// + /// use tick::{ClockControl, FutureExt}; + /// + /// # async fn example() -> Result<(), Box> { + /// let clock = ClockControl::new_auto_advancing().to_clock(); + /// let error = clock + /// .delay(Duration::from_secs(2)) + /// .timeout(&clock, Duration::from_secs(1)) + /// .await + /// .err() + /// .ok_or_else(|| std::io::Error::other("future unexpectedly completed"))?; + /// + /// assert!(error.is_timeout()); + /// # Ok(()) + /// # } + /// # } + /// ``` + #[must_use] + pub const fn is_timeout(&self) -> bool { + matches!(&self.kind, ErrorKind::Timeout) + } + + /// Returns whether this error reports a value outside a supported range. + #[must_use] + pub const fn is_out_of_range(&self) -> bool { + #[cfg(any(feature = "fmt", test))] + { + matches!(&self.kind, ErrorKind::OutOfRange(_)) + } + + #[cfg(not(any(feature = "fmt", test)))] + { + false + } + } + + /// Returns the captured backtrace, when backtrace capture was enabled. + #[must_use] + pub const fn backtrace(&self) -> Option<&Backtrace> { + self.backtrace.get() + } + #[cfg(test)] const fn kind(&self) -> &ErrorKind { - &self.0 + &self.kind } } impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match &self.0 { + match &self.kind { #[cfg(any(feature = "fmt", test))] ErrorKind::Jiff(err) => err.fmt(f), #[cfg(any(feature = "fmt", test))] ErrorKind::OutOfRange(msg) => write!(f, "{msg}"), ErrorKind::Other(err) => err.fmt(f), ErrorKind::SystemTimeError(err) => err.fmt(f), + ErrorKind::Timeout => f.write_str("future timed out"), } } } impl std::error::Error for Error { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - match &self.0 { + match &self.kind { #[cfg(any(feature = "fmt", test))] ErrorKind::Jiff(err) => Some(err), #[cfg(any(feature = "fmt", test))] ErrorKind::OutOfRange(_) => None, ErrorKind::Other(err) => Some(err.as_ref()), ErrorKind::SystemTimeError(err) => Some(err), + ErrorKind::Timeout => None, } } } impl From for Error { fn from(err: SystemTimeError) -> Self { - Self(ErrorKind::SystemTimeError(err)) + Self::from_kind(ErrorKind::SystemTimeError(err)) + } +} + +impl From for Error { + fn from(error: std::num::ParseIntError) -> Self { + Self::other(error) } } @@ -135,6 +241,8 @@ mod tests { let error = Error::out_of_range("test"); assert!(matches!(error.kind(), ErrorKind::OutOfRange(_))); + assert!(error.is_out_of_range()); + assert!(!error.is_timeout()); assert_eq!(error.to_string(), "test"); assert!(error.source().is_none()); } @@ -161,6 +269,36 @@ mod tests { assert!(error.source().is_some()); } + #[test] + fn timeout_error_is_classified() { + let error = Error::timeout(); + + assert!(error.is_timeout()); + assert!(!error.is_out_of_range()); + assert_eq!(error.to_string(), "future timed out"); + assert!(error.source().is_none()); + } + + #[test] + fn captured_backtrace_is_available() { + let error = Error { + kind: ErrorKind::Timeout, + backtrace: MaybeBacktrace::from_backtrace(Backtrace::force_capture()), + }; + + assert!(error.backtrace().is_some()); + } + + #[test] + fn disabled_backtrace_is_unavailable() { + let error = Error { + kind: ErrorKind::Timeout, + backtrace: MaybeBacktrace::from_backtrace(Backtrace::disabled()), + }; + + assert!(error.backtrace().is_none()); + } + #[test] fn thread_aware_ok() { let error = Error::other(std::io::Error::other("dummy")); diff --git a/crates/tick/src/fmt/mod.rs b/crates/tick/src/fmt/mod.rs index 7c5498bad..3e3b3162e 100644 --- a/crates/tick/src/fmt/mod.rs +++ b/crates/tick/src/fmt/mod.rs @@ -25,10 +25,9 @@ //! because the `SystemTime` can be outside the maximum range of the respective format. The conversion back to `SystemTime` is //! always infallible. //! -//! To retrieve the current system time in the respective format, use the [`Clock::system_time_as`][crate::Clock::system_time_as] function -//! which retrieves current system time and does the automatic conversion to the output format. It panics when the target format cannot -//! represent the clock's instant, which a controlled clock can produce; use [`Clock::system_time`][crate::Clock::system_time] with the -//! target type's [`TryFrom`] where that has to be handled. +//! To retrieve the current system time in the respective format, use +//! [`Clock::try_system_time_as`][crate::Clock::try_system_time_as]. It retrieves the current +//! system time and performs the target type's [`TryFrom`] conversion. //! //! # Representable range //! @@ -48,7 +47,8 @@ //! assert_eq!(Iso8601::try_from(system_time)?, iso); //! //! // `UnixSeconds` counts forward from the Unix epoch, so an earlier instant is rejected. -//! UnixSeconds::try_from(SystemTime::UNIX_EPOCH - std::time::Duration::from_secs(1)).unwrap_err(); +//! let before_epoch = SystemTime::UNIX_EPOCH - std::time::Duration::from_secs(1); +//! assert!(UnixSeconds::try_from(before_epoch).is_err()); //! //! # Ok::<(), Box>(()) //! ``` @@ -81,7 +81,7 @@ //! //! // The fallible step is explicit: `UnixSeconds` cannot express a pre-epoch instant. //! let before_epoch: Iso8601 = "1969-12-31T23:59:59Z".parse()?; -//! UnixSeconds::try_from(SystemTime::from(before_epoch)).unwrap_err(); +//! assert!(UnixSeconds::try_from(SystemTime::from(before_epoch)).is_err()); //! //! # Ok::<(), Box>(()) //! ``` @@ -198,10 +198,10 @@ mod tests { let clock = Clock::new_frozen_at(SystemTime::UNIX_EPOCH + Duration::from_millis(10_123_456)); let dates = Dates { - iso: clock.system_time_as::(), - rfc: clock.system_time_as::(), - unix: clock.system_time_as::(), - ecma: clock.system_time_as::(), + iso: clock.try_system_time_as::().unwrap(), + rfc: clock.try_system_time_as::().unwrap(), + unix: clock.try_system_time_as::().unwrap(), + ecma: clock.try_system_time_as::().unwrap(), }; let json = serde_json::to_string(&dates).unwrap(); @@ -217,10 +217,10 @@ mod tests { let clock = Clock::new_frozen_at(SystemTime::UNIX_EPOCH + Duration::from_millis(10_123_456)); let dates = Dates { - iso: clock.system_time_as::(), - rfc: clock.system_time_as::(), - unix: clock.system_time_as::(), - ecma: clock.system_time_as::(), + iso: clock.try_system_time_as::().unwrap(), + rfc: clock.try_system_time_as::().unwrap(), + unix: clock.try_system_time_as::().unwrap(), + ecma: clock.try_system_time_as::().unwrap(), }; let formatted = format!("iso: {}, unix: {}, rfc: {}, ecma: {}", dates.iso, dates.unix, dates.rfc, dates.ecma); @@ -235,10 +235,10 @@ mod tests { let clock = Clock::new_frozen_at(SystemTime::UNIX_EPOCH + Duration::from_secs(10123)); let dates = Dates { - iso: clock.system_time_as::(), - rfc: clock.system_time_as::(), - unix: clock.system_time_as::(), - ecma: clock.system_time_as::(), + iso: clock.try_system_time_as::().unwrap(), + rfc: clock.try_system_time_as::().unwrap(), + unix: clock.try_system_time_as::().unwrap(), + ecma: clock.try_system_time_as::().unwrap(), }; let json = serde_json::to_string(&dates).unwrap(); diff --git a/crates/tick/src/fmt/rfc_2822.rs b/crates/tick/src/fmt/rfc_2822.rs index 09eb85254..2371b866b 100644 --- a/crates/tick/src/fmt/rfc_2822.rs +++ b/crates/tick/src/fmt/rfc_2822.rs @@ -79,7 +79,7 @@ static MIN_TIMESTAMP: LazyLock = LazyLock::new(|| { /// // This string is ISO 8601, not RFC 2822, so the parser rejects it as malformed; RFC 2822 /// // cannot write a year outside `0000` through `9999` at all. The year bound itself is /// // enforced on the `TryFrom` path, where such an instant can arrive. -/// "-0001-06-15T00:00:00Z".parse::().unwrap_err(); +/// assert!("-0001-06-15T00:00:00Z".parse::().is_err()); /// ``` /// /// # Examples diff --git a/crates/tick/src/fmt/unix_seconds.rs b/crates/tick/src/fmt/unix_seconds.rs index c2b7fdf6d..315c97ae5 100644 --- a/crates/tick/src/fmt/unix_seconds.rs +++ b/crates/tick/src/fmt/unix_seconds.rs @@ -42,7 +42,7 @@ use crate::Error; /// /// let before_epoch = SystemTime::UNIX_EPOCH - Duration::from_secs(1); /// -/// UnixSeconds::try_from(before_epoch).unwrap_err(); +/// assert!(UnixSeconds::try_from(before_epoch).is_err()); /// ``` /// /// # Examples @@ -76,11 +76,7 @@ impl UnixSeconds { /// The largest value that can be represented by `UnixSeconds`. /// /// This represents `253402207200`, or `9999-12-30T22:00:00Z`. - #[expect( - clippy::duration_suboptimal_units, - reason = "UnixSeconds represents whole seconds since the Unix epoch, so seconds is the natural unit, matching to_secs() and Display" - )] - pub const MAX: Self = Self(Duration::from_secs(253_402_207_200)); + pub const MAX: Self = Self(Duration::from_hours(70_389_502)); /// The Unix epoch represented as `UnixSeconds`. /// @@ -97,7 +93,7 @@ impl UnixSeconds { /// ``` /// use tick::fmt::UnixSeconds; /// - /// UnixSeconds::from_secs(u64::MAX).unwrap_err(); + /// assert!(UnixSeconds::from_secs(u64::MAX).is_err()); /// ``` /// /// # Examples @@ -107,13 +103,15 @@ impl UnixSeconds { /// /// use tick::fmt::UnixSeconds; /// - /// let unix_seconds = UnixSeconds::from_secs(10).unwrap(); + /// let unix_seconds = UnixSeconds::from_secs(10)?; /// let system_time: SystemTime = unix_seconds.into(); /// /// assert_eq!( /// system_time, /// SystemTime::UNIX_EPOCH + Duration::from_secs(10) /// ); + /// + /// # Ok::<(), tick::Error>(()) /// ``` pub fn from_secs(seconds: u64) -> Result { Self::try_from(Duration::from_secs(seconds)).map_err(|_error| { @@ -141,7 +139,7 @@ impl FromStr for UnixSeconds { type Err = Error; fn from_str(s: &str) -> Result { - let secs: u64 = s.parse().map_err(Error::other)?; + let secs: u64 = s.parse()?; Self::from_secs(secs) } } diff --git a/crates/tick/src/future_ext.rs b/crates/tick/src/future_ext.rs index 8ce4a7d12..6f7e60436 100644 --- a/crates/tick/src/future_ext.rs +++ b/crates/tick/src/future_ext.rs @@ -19,18 +19,21 @@ pub trait FutureExt: Future { /// /// use tick::{Clock, FutureExt}; /// - /// # async fn timeout_example(clock: &Clock) { + /// # async fn timeout_example(clock: &Clock) -> Result<(), Box> { /// // Create a long-running future and apply a timeout /// let timeout_error = clock /// .delay(Duration::from_millis(700)) /// .timeout(&clock, Duration::from_millis(200)) /// .await - /// .unwrap_err(); + /// .err() + /// .ok_or_else(|| std::io::Error::other("future unexpectedly completed"))?; /// /// assert_eq!(timeout_error.to_string(), "future timed out"); + /// # Ok(()) /// # } /// ``` - fn timeout(self, clock: &Clock, timeout: Duration) -> Timeout + #[must_use = "futures do nothing unless awaited or polled"] + fn timeout(self, clock: &Clock, timeout: Duration) -> Timeout where Self: Sized, { @@ -52,9 +55,10 @@ mod tests { #[test] fn timeout_control() { - let control = ClockControl::new() + let control = ClockControl::builder() .auto_advance(Duration::from_secs(1)) - .auto_advance_limit(Duration::from_secs(2)); + .auto_advance_limit(Duration::from_secs(2)) + .build(); let clock = control.to_clock(); diff --git a/crates/tick/src/lib.rs b/crates/tick/src/lib.rs index 952fb30de..62cb1bf72 100644 --- a/crates/tick/src/lib.rs +++ b/crates/tick/src/lib.rs @@ -3,10 +3,6 @@ #![cfg_attr(all(coverage_nightly, test), feature(coverage_attribute))] #![cfg_attr(docsrs, feature(doc_cfg))] -#![cfg_attr( - not(all(feature = "test-util", feature = "tokio", feature = "fmt")), - expect(rustdoc::broken_intra_doc_links, reason = "simpler docs") -)] #![doc(html_logo_url = "https://media.githubusercontent.com/media/microsoft/oxidizer/refs/heads/main/crates/tick/logo.png")] #![doc(html_favicon_url = "https://media.githubusercontent.com/media/microsoft/oxidizer/refs/heads/main/crates/tick/favicon.ico")] #![cfg_attr( @@ -35,12 +31,15 @@ //! 123 //! } //! +//! # #[cfg(feature = "tokio")] //! #[tokio::main] //! async fn main() { //! let clock = Clock::new_tokio(); //! let value = produce_value(&clock).await; //! assert_eq!(value, 123); //! } +//! # #[cfg(not(feature = "tokio"))] +//! # fn main() {} //! //! #[cfg(test)] //! mod tests { @@ -51,7 +50,7 @@ //! #[tokio::test] //! async fn test_produce_value() { //! // Automatically advance timers for instant, deterministic testing -//! let clock: Clock = ClockControl::new().auto_advance_timers(true).to_clock(); +//! let clock: Clock = ClockControl::new_auto_advancing().to_clock(); //! assert_eq!(produce_value(&clock).await, 123); //! } //! } @@ -150,8 +149,11 @@ //! //! # Thread-aware relocation //! -//! All clock types implement [`ThreadAware`](thread_aware::ThreadAware), supporting per-core -//! timer isolation in thread-per-core runtime architectures. +//! [`Clock`], [`SimpleClock`], [`ClockControl`], and +//! [`InactiveClock`][runtime::InactiveClock] implement +//! [`ThreadAware`](thread_aware::ThreadAware), supporting per-core timer isolation in +//! thread-per-core runtime architectures. [`InactiveClock`][runtime::InactiveClock] +//! deliberately does not implement `ThreadAware` because one driver owns its shared timer set. //! //! When an [`InactiveClock`][runtime::InactiveClock] is //! [relocated](thread_aware::ThreadAware::relocate) to a target thread, the underlying timer @@ -266,11 +268,20 @@ //! - **`serde`** - Adds serialization and deserialization support via [serde](https://serde.rs/). //! - **`fmt`** - Enables the [`fmt`] module with utilities for formatting `SystemTime` into //! various formats (e.g., ISO 8601, RFC 2822). +//! - **`rt-shared`** - Enables +//! [`InactiveClock::new_shared`](https://docs.rs/tick/latest/tick/runtime/struct.InactiveClock.html#method.new_shared) +//! for runtimes that use one shared timer set and one driver. //! //! # Additional Examples //! //! The [time examples](https://github.com/microsoft/oxidizer/tree/main/crates/tick/examples) //! contain additional examples of how to use the time primitives. +//! +//! [`ClockControl`]: https://docs.rs/tick/latest/tick/struct.ClockControl.html +//! [`ClockControl::to_simple_clock`]: https://docs.rs/tick/latest/tick/struct.ClockControl.html#method.to_simple_clock +//! [`Clock::new_tokio`]: https://docs.rs/tick/latest/tick/struct.Clock.html#method.new_tokio +//! [`fmt`]: https://docs.rs/tick/latest/tick/fmt/index.html +//! [`SystemTimeExt`]: https://docs.rs/tick/latest/tick/trait.SystemTimeExt.html mod clock; #[cfg(any(feature = "test-util", test))] @@ -286,6 +297,7 @@ mod periodic_timer; mod simple_clock; mod state; mod stopwatch; +#[cfg(any(feature = "fmt", test))] mod system_time_ext; mod timers; @@ -293,13 +305,14 @@ pub mod runtime; pub(crate) mod timeout; pub use clock::Clock; #[cfg(any(feature = "test-util", test))] -pub use clock_control::ClockControl; +pub use clock_control::{ClockControl, ClockControlBuilder}; pub use delay::Delay; pub use error::{Error, Result}; pub use future_ext::FutureExt; pub use periodic_timer::PeriodicTimer; pub use simple_clock::SimpleClock; pub use stopwatch::Stopwatch; +#[cfg(any(feature = "fmt", test))] pub use system_time_ext::SystemTimeExt; pub use timeout::Timeout; diff --git a/crates/tick/src/periodic_timer.rs b/crates/tick/src/periodic_timer.rs index af67e5739..c2aae0b4f 100644 --- a/crates/tick/src/periodic_timer.rs +++ b/crates/tick/src/periodic_timer.rs @@ -78,6 +78,7 @@ use crate::timers::TIMER_RESOLUTION; /// # } /// ``` #[derive(Debug)] +#[must_use = "streams do nothing unless polled"] pub struct PeriodicTimer { period: Duration, clock: Clock, @@ -91,7 +92,7 @@ impl PeriodicTimer { /// /// > **Note**: The minimum precision of the timer is 1ms. If a smaller period is specified, /// > it will be adjusted to 1ms. - #[must_use] + #[must_use = "streams do nothing unless polled"] pub fn new(clock: &Clock, period: Duration) -> Self { let period = period.max(TIMER_RESOLUTION); @@ -142,7 +143,10 @@ impl Stream for PeriodicTimer { Poll::Ready(Some(())) } // Timer is registered and will fire later in the future. - Some(_) => Poll::Pending, + Some(key) => { + this.clock.update_timer_waker(key, cx.waker()); + Poll::Pending + } // Timer is not registered yet; let's register it. // The registration is lazy, occurring when someone polls the future. This means @@ -167,6 +171,9 @@ impl Drop for PeriodicTimer { #[cfg_attr(coverage_nightly, coverage(off))] #[cfg(test)] mod tests { + use std::sync::Arc; + use std::sync::atomic::{AtomicUsize, Ordering}; + use std::task::Wake; use std::thread; use super::*; @@ -275,11 +282,42 @@ mod tests { assert_eq!(clock.clock_state().timers_len(), 0); } + #[test] + fn latest_waker_is_notified() { + let control = ClockControl::new(); + let clock = control.to_clock(); + let mut timer = PeriodicTimer::new(&clock, Duration::from_secs(1)); + let first = Arc::new(WakeCounter::default()); + let second = Arc::new(WakeCounter::default()); + + assert_eq!(poll_timer_with_waker(&mut timer, &Waker::from(Arc::clone(&first))), Poll::Pending); + assert_eq!(poll_timer_with_waker(&mut timer, &Waker::from(Arc::clone(&second))), Poll::Pending); + + control.advance(Duration::from_secs(1)); + + assert_eq!(first.count.load(Ordering::Relaxed), 0); + assert_eq!(second.count.load(Ordering::Relaxed), 1); + } + fn poll_timer(delay: &mut PeriodicTimer) -> Poll> { - let waker = Waker::noop().clone(); - let mut cx = Context::from_waker(&waker); + poll_timer_with_waker(delay, Waker::noop()) + } + + fn poll_timer_with_waker(delay: &mut PeriodicTimer, waker: &Waker) -> Poll> { + let mut cx = Context::from_waker(waker); let delay = std::pin::pin!(delay); delay.poll_next(&mut cx) } + + #[derive(Default)] + struct WakeCounter { + count: AtomicUsize, + } + + impl Wake for WakeCounter { + fn wake(self: Arc) { + self.count.fetch_add(1, Ordering::Relaxed); + } + } } diff --git a/crates/tick/src/runtime/inactive_clock.rs b/crates/tick/src/runtime/inactive_clock.rs index 95abc686d..8ef6c3bdc 100644 --- a/crates/tick/src/runtime/inactive_clock.rs +++ b/crates/tick/src/runtime/inactive_clock.rs @@ -21,7 +21,8 @@ pub struct Isolated; /// Marker for an [`InactiveClock`] backed by a single shared timer set. /// -/// Created via [`InactiveClock::new_shared`]. The resulting `InactiveClock` +/// Created via [`InactiveClock::new_shared`](https://docs.rs/tick/latest/tick/runtime/struct.InactiveClock.html#method.new_shared). +/// The resulting `InactiveClock` /// intentionally does **not** implement [`Clone`] or [`ThreadAware`]: there is exactly one /// shared timer set advanced by exactly one driver, so cloning or relocation would create /// configurations the driver could not advance correctly. @@ -38,7 +39,8 @@ pub struct Shared; /// relocated across threads, with each thread getting an independent timer set on /// activation. /// - [`Shared`]: a single shared timer set advanced by a single driver. Use -/// [`InactiveClock::new_shared`] to construct one. Does not implement [`Clone`] or +/// [`InactiveClock::new_shared`](https://docs.rs/tick/latest/tick/runtime/struct.InactiveClock.html#method.new_shared) +/// to construct one. Does not implement [`Clone`] or /// [`ThreadAware`]. /// /// To begin using the clock, call [`InactiveClock::activate`] to get a working [`Clock`] instance and @@ -103,7 +105,7 @@ impl InactiveClock { /// The returned value is intentionally not [`Clone`] and does not implement /// [`ThreadAware`]: a `Shared` clock has exactly one timer set that must be advanced by /// exactly one [`ClockDriver`]. This is the construction mode used by - /// [`Clock::new_tokio`][crate::Clock::new_tokio]. + /// [`Clock::new_tokio`](https://docs.rs/tick/latest/tick/struct.Clock.html#method.new_tokio). #[must_use] pub fn new_shared() -> Self { Self { diff --git a/crates/tick/src/simple_clock.rs b/crates/tick/src/simple_clock.rs index e2e555791..3a475b910 100644 --- a/crates/tick/src/simple_clock.rs +++ b/crates/tick/src/simple_clock.rs @@ -18,9 +18,9 @@ use crate::thread_aware_move; /// - [`Clock`][crate::Clock] implements [`AsRef`] and exposes /// [`Clock::simple_clock`][crate::Clock::simple_clock], so a timer-capable clock can be used /// anywhere a `SimpleClock` is expected. -/// - With the `test-util` feature, [`ClockControl::to_simple_clock`][crate::ClockControl::to_simple_clock] +/// - With the `test-util` feature, [`ClockControl::to_simple_clock`] /// creates a controlled `SimpleClock` whose time is driven by the same -/// [`ClockControl`][crate::ClockControl]. +/// [`ClockControl`]. /// /// This makes APIs that only need to read time — such as [`Stopwatch`][crate::Stopwatch] — /// seamlessly accept either kind of clock. @@ -37,6 +37,9 @@ use crate::thread_aware_move; /// /// assert!(second >= first); /// ``` +/// +/// [`ClockControl`]: https://docs.rs/tick/latest/tick/struct.ClockControl.html +/// [`ClockControl::to_simple_clock`]: https://docs.rs/tick/latest/tick/struct.ClockControl.html#method.to_simple_clock #[derive(Debug, Clone)] pub struct SimpleClock(TimeKind); @@ -146,29 +149,28 @@ impl SimpleClock { } } - /// Retrieves the current system time converted to a target type. + /// Retrieves the current system time and converts it to `T`. /// - /// See [`Clock::system_time_as`][crate::Clock::system_time_as] for details. + /// # Errors /// - /// # Panics + /// Returns the target type's conversion error when the current [`SystemTime`] is outside + /// the target type's representable range. /// - /// Panics if the current system time cannot be represented by the target type. - /// This can happen if the target type supports a narrower range than `SystemTime`, or in tests - /// when controlled time is moved outside the target type's supported range. - #[expect( - clippy::match_wild_err_arm, - clippy::panic, - reason = "conversion failure indicates the chosen target type cannot represent the current SystemTime (or, in tests, controlled time was moved out of range); panicking keeps this API infallible" - )] - #[must_use] - pub fn system_time_as>(&self) -> T { - match T::try_from(self.system_time()) { - Ok(time) => time, - Err(_err) => panic!( - "system_time_as::<{}> failed: target type cannot represent the current SystemTime (or controlled time is out of range)", - std::any::type_name::() - ), - } + /// # Examples + /// + /// ``` + /// use std::time::SystemTime; + /// + /// use tick::SimpleClock; + /// + /// let clock = SimpleClock::new_system(); + /// let time = clock.try_system_time_as::()?; + /// assert!(time <= SystemTime::now()); + /// + /// # Ok::<(), std::convert::Infallible>(()) + /// ``` + pub fn try_system_time_as>(&self) -> Result { + T::try_from(self.system_time()) } /// Retrieves the current [`Instant`]. diff --git a/crates/tick/src/state.rs b/crates/tick/src/state.rs index 14e75af3e..6b336694c 100644 --- a/crates/tick/src/state.rs +++ b/crates/tick/src/state.rs @@ -7,7 +7,7 @@ use std::time::Instant; use thread_aware::affinity::Affinity; use thread_aware::{PerCore, ThreadAware}; -use crate::timers::Timers; +use crate::timers::{ReadyTimers, Timers}; #[derive(Debug, Clone)] pub(crate) enum ClockState { @@ -123,7 +123,15 @@ impl SynchronizedTimers { #[cfg_attr(test, mutants::skip)] // Causes test timeout. pub(crate) fn try_advance_timers(&self, now: Instant) -> Option { - self.with_timers(|timers| timers.advance_timers(now)) + let mut ready = ReadyTimers::new(); + let next = self.with_timers(|timers| timers.advance_timers(now, &mut ready)); + + // A wake may synchronously re-enter timer code, so invoke it after releasing the lock. + for waker in ready.into_values() { + waker.wake(); + } + + next } #[cfg_attr(test, mutants::skip)] // causes test timeout diff --git a/crates/tick/src/stopwatch.rs b/crates/tick/src/stopwatch.rs index 2f3b63795..a880e6d15 100644 --- a/crates/tick/src/stopwatch.rs +++ b/crates/tick/src/stopwatch.rs @@ -66,18 +66,6 @@ impl Stopwatch { } } -impl From for Instant { - fn from(stopwatch: Stopwatch) -> Self { - stopwatch.start - } -} - -impl From for Duration { - fn from(stopwatch: Stopwatch) -> Self { - stopwatch.elapsed() - } -} - #[cfg(test)] mod test { use std::thread::sleep; @@ -128,24 +116,4 @@ mod test { control.advance(Duration::from_secs(1)); assert_eq!(watch.elapsed(), Duration::from_secs(1)); } - - #[test] - fn test_stopwatch_into_instance() { - let clock = Clock::new_frozen(); - let watch = clock.stopwatch(); - - let instant: Instant = watch.into(); - assert_eq!(instant, clock.instant()); - } - - #[test] - fn test_stopwatch_into_duration() { - let control = ClockControl::new(); - let clock = control.to_clock(); - let watch = clock.stopwatch(); - control.advance(Duration::from_secs(1)); - - let duration: Duration = watch.into(); - assert_eq!(duration, Duration::from_secs(1)); - } } diff --git a/crates/tick/src/timeout.rs b/crates/tick/src/timeout.rs index a5f7bb721..01983ccf0 100644 --- a/crates/tick/src/timeout.rs +++ b/crates/tick/src/timeout.rs @@ -1,35 +1,37 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -use std::io::ErrorKind; use std::pin::Pin; use std::task::{Context, Poll}; use pin_project_lite::pin_project; -use crate::Error; +use crate::{Delay, Error}; pin_project! { /// A future that races between an inner future and a deadline. /// /// - If the inner future completes before the deadline, the future's output is returned. /// - If the deadline is reached before the inner future completes, an error is returned. + /// + /// Values are created by [`FutureExt::timeout`](crate::FutureExt::timeout). #[derive(Debug)] - pub struct Timeout { + #[must_use = "futures do nothing unless awaited or polled"] + pub struct Timeout { #[pin] future: F, #[pin] - deadline: D, + deadline: Delay, } } -impl Timeout { - pub(super) const fn new(future: F, deadline: D) -> Self { +impl Timeout { + pub(super) const fn new(future: F, deadline: Delay) -> Self { Self { future, deadline } } } -impl Future for Timeout { +impl Future for Timeout { type Output = Result; fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { @@ -38,10 +40,7 @@ impl Future for Timeout { match this.future.poll(cx) { Poll::Ready(v) => Poll::Ready(Ok(v)), Poll::Pending => match this.deadline.poll(cx) { - Poll::Ready(_) => { - let io_err = std::io::Error::new(ErrorKind::TimedOut, "future timed out"); - Poll::Ready(Err(Error::other(io_err))) - } + Poll::Ready(()) => Poll::Ready(Err(Error::timeout())), Poll::Pending => Poll::Pending, }, } diff --git a/crates/tick/src/timers.rs b/crates/tick/src/timers.rs index 6a37b25ba..49ddbfc75 100644 --- a/crates/tick/src/timers.rs +++ b/crates/tick/src/timers.rs @@ -34,6 +34,8 @@ impl TimerKey { /// timer checks, while setting it too high would reduce timer precision. pub(crate) const TIMER_RESOLUTION: Duration = Duration::from_millis(1); +pub(crate) type ReadyTimers = BTreeMap; + /// Management of one-shot timers, inspired by the [glommio runtime](https://github.com/DataDog/glommio/blob/d3f6e7a2ee7fb071ada163edcf90fc3286424c31/glommio/src/reactor.rs#L80). /// /// The timers managed by this collection are one-shot, meaning they will not fire again after being triggered. @@ -86,6 +88,17 @@ impl Timers { self.wakers.remove(&id); } + /// Replaces the waker for an existing timer when the awaiting task changes. + pub(crate) fn update_waker(&mut self, id: TimerKey, waker: &Waker) { + let Some(current) = self.wakers.get_mut(&id) else { + return; + }; + + if !current.will_wake(waker) { + current.clone_from(waker); + } + } + /// Returns the instant when the next timer will fire, or `None` if no timers are registered. pub(crate) fn next_timer(&self) -> Option { self.wakers.keys().next().map(TimerKey::tick) @@ -98,26 +111,22 @@ impl Timers { /// In the future, the signature of this method can be easily expanded to return more /// information about the timers that fired and when the next timer fires. #[cfg_attr(test, mutants::skip)] // Causes test timeout. - pub(crate) fn advance_timers(&mut self, now: Instant) -> Option { + pub(crate) fn advance_timers(&mut self, now: Instant, ready: &mut ReadyTimers) -> Option { self.alive = true; - // We are adding 1ns to the instant to ensure that even timers whose deadline is the current - // instant are advanced. This is required because of how BTreeMap::split_off works; it does - // not include keys that are equal to the split key. Adding 1ns to the value makes this work. - let adjusted_now = now.checked_add(Duration::from_nanos(1)).unwrap_or(now); - // Check if there are any timers that are ready to be woken. match self.wakers.first_entry() { Some(entry) => { - if entry.key().tick() <= adjusted_now { + if entry.key().tick() <= now { // Split timers into ready and pending timers. - let pending = self.wakers.split_off(&TimerKey::new(adjusted_now, 0)); - let ready = mem::replace(&mut self.wakers, pending); - - // Invoke the wakers for timers that ticked. - for (_, waker) in ready { - waker.wake(); - } + let mut expired = match now.checked_add(Duration::from_nanos(1)) { + Some(after_now) => { + let pending = self.wakers.split_off(&TimerKey::new(after_now, 0)); + mem::replace(&mut self.wakers, pending) + } + None => mem::take(&mut self.wakers), + }; + ready.append(&mut expired); // Return the next timer to be fired. return self.next_timer(); @@ -149,7 +158,7 @@ mod tests { assert_ne!(key1, key2); - timers.advance_timers(when + Duration::from_secs(1)); + advance_and_wake(&mut timers, when + Duration::from_secs(1)); assert_eq!(timers.len(), 0); } @@ -164,11 +173,11 @@ mod tests { let _id2 = timers.register(timer_second, Waker::noop().clone()); assert_eq!(timers.len(), 2); - timers.advance_timers(timer_first + Duration::from_nanos(1)); + advance_and_wake(&mut timers, timer_first + Duration::from_nanos(1)); assert_eq!(timers.len(), 1); assert!(!timers.contains(id1)); - timers.advance_timers(timer_second + Duration::from_nanos(1)); + advance_and_wake(&mut timers, timer_second + Duration::from_nanos(1)); assert_eq!(timers.len(), 0); } @@ -221,12 +230,64 @@ mod tests { fn advance_timers_ensure_correct_result() { let mut timers = Timers::default(); let now = Instant::now(); - assert!(timers.advance_timers(now).is_none()); + assert!(advance_and_wake(&mut timers, now).is_none()); let next = now.checked_add(Duration::from_secs(1)).unwrap(); let _ = timers.register(next, Waker::noop().clone()); - assert_eq!(timers.advance_timers(now), Some(next)); + assert_eq!(advance_and_wake(&mut timers, now), Some(next)); + + assert_eq!(advance_and_wake(&mut timers, next), None); + } + + #[test] + fn advance_timers_at_maximum_instant() { + let mut timers = Timers::default(); + let maximum = maximum_instant_after(Instant::now()); + let _ = timers.register(maximum, Waker::noop().clone()); + + assert!(advance_and_wake(&mut timers, maximum).is_none()); + assert_eq!(timers.len(), 0); + } + + fn advance_and_wake(timers: &mut Timers, now: Instant) -> Option { + let mut ready = ReadyTimers::new(); + let next = timers.advance_timers(now, &mut ready); + + for waker in ready.into_values() { + waker.wake(); + } + + next + } + + fn maximum_instant_after(anchor: Instant) -> Instant { + let mut lower = 0; + let mut upper = Duration::MAX.as_nanos(); + + while lower < upper { + let middle = lower + (upper - lower).div_ceil(2); + if anchor.checked_add(duration_from_nanos(middle)).is_some() { + lower = middle; + } else { + upper = middle - 1; + } + } + + anchor + .checked_add(duration_from_nanos(lower)) + .expect("binary search only retains durations that can be added to the anchor") + } - assert_eq!(timers.advance_timers(next), None); + fn duration_from_nanos(nanos: u128) -> Duration { + const NANOS_PER_SECOND: u128 = 1_000_000_000; + + Duration::new( + (nanos / NANOS_PER_SECOND) + .try_into() + .expect("Duration::MAX bounds the seconds component to u64"), + (nanos % NANOS_PER_SECOND) + .try_into() + .expect("the nanoseconds remainder is always less than one billion"), + ) } }