dotnet / dotnet/extensions

Ambiguity in the FakeTimeProvider timer invocations

Open
#6,810 3 comments 0 reactions 0 assignees View on GitHub
area-fundamentals bug untriaged
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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.