Skip to content

hyper-util: regression in paused time compatibility #3950

@asmello

Description

@asmello

Version
hyper version 1.7.0 (not relevant AFAIK), hyper-util version 0.1.16

Platform
MacOS

Description

Now, I realise nowhere (that I know) Hyper promises to be compatible with Tokio's paused-time feature. But neither does it document (that I know) that it isn't.

The issue I'm reporting is fundamentally due to an assumption that hyper-util makes that std::time::Instant always matches tokio::time::Instant. This is not true when paused time is enabled.

This probably spans multiple places, but let's focus on the one I've investigated: the IdleTask implementation.

  1. Initially std::time::Instant and tokio::time::Instant start the same.
  2. When IdleTask is first polled, its internal tokio::time::Sleep is configured to trigger after std::time::Instant::now() plus the timeout interval. Since the clocks are still in sync (assuming it gets polled immediately, which seems to be the case empirically), this does the correct thing and the deadline is set 90s in the future (assuming the default timeout is used) according to both clocks.
  3. Tokio will let other tasks do work, and once an idle state is reached, it will auto-advance time until the next scheduled event. Let's suppose this is the deadline set by IdleTask.
  4. Now over 90s have passed in Tokio's clock, but very little real time has actually passed. The clocks have drifted. When we configure the next deadline here, it will be based off actual time. The actual time plus 90s is going to be very close to Tokio's perceived present, maybe even in the past. This causes the task to be immediately awaken and the future polled again.
  5. In the next iteration, very little actual time will have passed, so the next deadline is still approximately 90s past the starting time. Again this is in the present/past from Tokio's perspective, so again it schedules the future to be polled immediately.
  6. This creates a busy loop. The program hangs.

Interestingly, paused time used to almost work up to release 0.1.15. This is because the next deadline was calculated relative to the previous one, rather than the realtime clock. The drift check in L801 would never trigger, because it used real time, which after the first deadline was always set in the past from Tokio's perspective. Since the two clocks started in sync, and Tokio was nice enough to wake up the task at the right times, the previous deadline + timeout interval were approximately equal to tokio::time::Instant::now() at each iteration, so the deadlines were set more or less correctly anyway.

I think even prior to 0.1.16 enough drift between computed deadlines (always spaced exactly this.duration apart) and Tokio's simulated time would accumulate that you'd eventually hit the drift check at L801, and then it would hit a busy loop all the same. But that was relatively unlikely, since paused time is mostly used for testing, and tests tend to not run over that long (even in simulated time).

Suggested fix

I believe the right fix for this is to not transparently convert between std::time and tokio::time types like here and here. The same problem would likely occur in other runtimes if they diverged at all from std::times monotonic clock implementation.

Instead, hyper-util should probably use an abstraction like what it does for the Sleep methods, where it defers to a boxed trait object for the actual implementation. So instead of directly calling std::time::Instant::now(), it should call some TimeProvider::now() method that can be configured to one runtime or another.

Alternatively, the logic for determining and setting the next deadline could be moved to the runtime-specific implementations, so that IdleTask would never have to directly manipulate Instants.

I think this part of the codebase is currently being revised, so maybe this is not worth fixing. But I figured I'd at least report the perceived regression — I've observed tests that were working just fine break after the 0.1.16 patch release. Even if paused time compatibility is not part of hyper-utils promises, this isn't ideal.

Metadata

Metadata

Assignees

No one assigned

    Labels

    C-bugCategory: bug. Something is wrong. This is bad!K-hyper-utilCrate: hyper-util

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions