AsyncSchedulerBase's absolute scheduling does not cope with clock drift
Background
The IAsyncScheduler defines a method allowing work to be scheduled at a specific time in the future:
ValueTask<IAsyncDisposable> ScheduleAsync(Func<CancellationToken, ValueTask> action, DateTimeOffset dueTime);This is one of three overloads the interface defines. There's this absolute time one, and also a relative time (TimeSpan) and an immediate one.
The AsyncSchedulerBase only requires derived types to implement a single, immediate method:
protected abstract ValueTask ScheduleAsyncCore(Func<CancellationToken, ValueTask> action, CancellationToken token);It implements the two time-based versions for you with async methods that await until the due time. To enable derived types to define how time passes, they also implement this method:
protected abstract ValueTask Delay(TimeSpan dueTime, CancellationToken token);This is fine for virtual time, but it is problematic for real time: it does not cope with changes to system time. If the system time as adjusted, work scheduled for an absolute time will seem to run at the wrong time. To see why, look at the implementation of the DateTimeOffset dueTime overload of ScheduleAsync:
return ScheduleAsyncCore(async ct =>
{
var dueTimeRelative = Normalize(dueTime - Now); // TODO: Support clock drift and clock changes.
await Delay(dueTimeRelative, ct); // NB: Honor SynchronizationContext to stay on scheduler.
await action(ct);
});The comment shows that the original author was aware of this shortcoming. The problem is that this converts the absolute time to a TimeSpan, which means if the system clock changes after this was scheduled, that time change won't be taken into account.
This could cause work to be processed in the wrong order. Suppose the following happens:
- System time is 09:00:00+0
- Work scheduled for 09:01:00+0: converted to delay of 60 seconds
- One second later, the system clock is adjusted to 08:59:30+0
- Work scheduled for 09:00:50+0, converted to a delay of 80 seconds
- 59 seconds later (system time 09:00:29+0), the work scheduled for 09:01:00+0 runs
- 21 seconds later (system time 09:00:50+0) the work scheduled for 09:00:50+0 runs
So work runs out of order here. The second work item to run was scheduled to run 10 seconds earlier than the first one but actually ran 21 seconds later. To avoid this we'd need to detect clock changes (e.g. by looking at the top of the queue and seeing if its scheduled time still corresponds to the one we'd calculate if scheduling it right now).
Review what classic Rx does here - it might be that we don't solve the problem correctly there, in which case the resolution might simply be to document that absolute scheduling is just like this.
Source: dotnet/reactive