Ambiguity in the FakeTimeProvider timer invocations
- Dominant language
- C#
- Stars
- 3.2k
- Forks
- 894
- Avg merge
- 1d 12h
- Merged PRs (30d)
- 23
Description
### Description
Hi, recently I've found that `FakeTimeProvider` behavior may differ depending on when and where `FakeTimeProvider.Advance()` method is called.
I have a question about that case.
### Reproduction Steps
Two simple xUnit tests that are expected to be equal semantically: I expect the timer to be executed 10 times in both tests:
Test1:
```csharp
[Fact]
public void Ambiguous1()
{
var tp = new FakeTimeProvider();
int counter = 0;
const int samples = 10;
tp.CreateTimer(_ =>
{
Interlocked.Increment(ref counter);
}, null, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(1));
tp.Advance(TimeSpan.FromSeconds(samples));
// Immediate invocation + sampled.
Assert.Equal(samples, counter);
}
// OK
```
Test2:
```csharp
[Fact]
public void Ambiguous2()
{
var tp = new FakeTimeProvider();
int counter = 0;
const int samples = 10;
tp.CreateTimer(_ =>
{
int c = Interlocked.Increment(ref counter);
if (c >= samples)
return;
tp.Advance(TimeSpan.FromSeconds(1));
}, null, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(1));
tp.Advance(TimeSpan.FromSeconds(1));
// Immediate invocation + sampled.
Assert.Equal(samples, counter);
}
// Assert.Equal() Failure: Values differ
// Expected: 10
// Actual: 1
```
The main difference here is that, in the first test I instantly advance time by 10 seconds. While in the second test, time is advanced 10 times iteratively by one second from the inside of the timer callback.
As a result, first test timer is executed 10 times, while second test timer is executed only once.
While I can understand customized behavior of the `FakeTimeProvider`, this example looks still weird - we passed same amount of time, but got different number of invocations.
I've dived in `FakeTimeProvider` sources a bit, and have found [the next condition in `WakeWaiters()` method](https://github.com/dotnet/extensions/blob/b8caf954ba0f4d31d28819c20b52b1174f9ec8fd/src/Libraries/Microsoft.Extensions.TimeProvider.Testing/FakeTimeProvider.cs#L295):
```csharp
if (oldTicks != newTicks)
{
// time changed while in the callback, readjust the wake time accordingly
candidate.WakeupTime = newTicks + candidate.Period;
}
else
{
// move on to the next period
candidate.WakeupTime += candidate.Period;
}
```
If current time was changed during the current execution of the particular timer, then next timer wakeup time is immediately set to `new current time + period`; otherwise, timer's period iteratively added to the wakeup time.
This behavior exactly given the ambiguity I've described above. If I eliminate this condition (just keep `candidate.WakeupTime += candidate.Period;` in both cases), then both tests pass correctly.
I understand that eliminating this condition may change things dramatically. But the ambiguity looks weird also. So, I wanted to discuss that behavior or may be somebody could explain such conditional design decision and how to properly handle such cases.
Possibly related to #5722 also.
My very first issue here, so, I'm sorry for any inconvenience :)
### Expected behavior
Expected the number of timer callback execution times is equal in both tests.
### Actual behavior
The number of timer callback execution times differs.
### Regression?
_No response_
### Known Workarounds
_No response_
### Configuration
Microsoft.Extensions.TimeProvider.Testing v9.4.0
### Other information
_No response_
Contributor guide
Assessment
This issue has not been assessed yet.