Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions IntervalAction.Test/IntervalActionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
{
Interlocked.Increment(ref executions);
// Simulate a long running task.
Thread.Sleep(500);

Check warning on line 49 in IntervalAction.Test/IntervalActionTests.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Do not use 'Thread.Sleep()' in a test.

Check warning on line 49 in IntervalAction.Test/IntervalActionTests.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Do not use 'Thread.Sleep()' in a test.

Check warning on line 49 in IntervalAction.Test/IntervalActionTests.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Do not use 'Thread.Sleep()' in a test.

Check warning on line 49 in IntervalAction.Test/IntervalActionTests.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Do not use 'Thread.Sleep()' in a test.
},
IntervalType = IntervalType.FromLastStart
};
Expand Down Expand Up @@ -149,6 +149,48 @@
intervalAction.RethrowExceptions();
}

[TestMethod]
public async Task RestartRightAfterStopLeavesOnlyOnePollingLoop()
{
// Arrange
int counter = 0;
TimeSpan pollingInterval = TimeSpan.FromMilliseconds(200);
IntervalActionOptions options = new()
{
PollingInterval = pollingInterval,
ActionInterval = TimeSpan.Zero,
Action = () => Interlocked.Increment(ref counter),
IntervalType = IntervalType.FromLastStart
};

IntervalAction intervalAction = IntervalAction.Start(options);
// Let the loop run its first tick and enter its delay.
await Task.Delay(50).ConfigureAwait(false);

Check warning on line 168 in IntervalAction.Test/IntervalActionTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Consider using the overload that accepts a CancellationToken and pass 'TestContext.CancellationToken'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_IntervalAction&issues=AaDhk-Y0TFIbZ_TpklgN&open=AaDhk-Y0TFIbZ_TpklgN&pullRequest=55

// Act: stop, then restart while the old loop is still inside its delay
Task oldPollingTask = intervalAction.PollingTask;
intervalAction.Stop();
await intervalAction.RestartAsync().ConfigureAwait(false);

// Assert: the old loop has ended rather than being left to resume alongside the new one
Assert.IsTrue(oldPollingTask.IsCompleted, "Old polling task should have completed before the restart started a new one");
Assert.AreNotSame(oldPollingTask, intervalAction.PollingTask);

int counterAtRestart = Volatile.Read(ref counter);
System.Diagnostics.Stopwatch stopwatch = System.Diagnostics.Stopwatch.StartNew();
await Task.Delay(TimeSpan.FromTicks(pollingInterval.Ticks * 10)).ConfigureAwait(false);

Check warning on line 181 in IntervalAction.Test/IntervalActionTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Consider using the overload that accepts a CancellationToken and pass 'TestContext.CancellationToken'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_IntervalAction&issues=AaDhk-Y0TFIbZ_TpklgO&open=AaDhk-Y0TFIbZ_TpklgO&pullRequest=55
intervalAction.Stop();
await intervalAction.PollingTask.ConfigureAwait(false);
stopwatch.Stop();

// One loop starts the action at most once per polling interval, plus its first tick
int executions = Volatile.Read(ref counter) - counterAtRestart;
int maxExecutionsForOneLoop = (int)(stopwatch.Elapsed.Ticks / pollingInterval.Ticks) + 1;
Assert.IsLessThanOrEqualTo(maxExecutionsForOneLoop, executions, "The action ran faster than a single polling loop allows");

intervalAction.RethrowExceptions();
}

[TestMethod]
public async Task StopPollingTaskStopsExecuting()
{
Expand Down
75 changes: 33 additions & 42 deletions IntervalAction/IntervalAction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -150,18 +150,11 @@ private async Task RestartCoreAsync(Task previousRestart)
{
await WaitAndDiscardOutcomeAsync(previousRestart).ConfigureAwait(false);

bool shouldPoll;

lock (Lock)
{
shouldPoll = ShouldPoll;
}

if (shouldPoll)
{
Stop();
await WaitAndDiscardOutcomeAsync(PollingTask).ConfigureAwait(false);
}
// Wait for the old loop whether or not it is still meant to be polling. After Stop() it can
// still be inside its delay, and would see ShouldPoll set again below and keep running
// alongside the new loop, unreferenced.
Stop();
await WaitAndDiscardOutcomeAsync(PollingTask).ConfigureAwait(false);

lock (Lock)
{
Expand Down Expand Up @@ -208,50 +201,48 @@ internal bool TryRun()
{
Ensure.NotNull(Action);

if (ActionTask?.IsCompleted ?? false)
// Check and claim ActionTask under the lock, so two callers can never both see it empty and
// each start the action
lock (Lock)
{
if (ActionTask.Exception is not null)
if (ActionTask?.IsCompleted ?? false)
{
throw ActionTask.Exception.GetBaseException();
}

ActionTask = null;
}

DateTimeOffset lastRunTime;
if (ActionTask.Exception is not null)
{
throw ActionTask.Exception.GetBaseException();
}

lock (Lock)
{
lastRunTime = LastRunTime;
}
ActionTask = null;
}

if (ActionInterval >= TimeSpan.Zero && ActionTask is null && DateTimeOffset.Now - lastRunTime > ActionInterval)
{
ActionTask = Task.Run(() =>
if (ActionInterval >= TimeSpan.Zero && ActionTask is null && DateTimeOffset.Now - LastRunTime > ActionInterval)
{
if (IntervalType == IntervalType.FromLastStart)
ActionTask = Task.Run(() =>
{
lock (Lock)
if (IntervalType == IntervalType.FromLastStart)
{
LastRunTime = DateTimeOffset.Now;
lock (Lock)
{
LastRunTime = DateTimeOffset.Now;
}
}
}

Action();
Action();

if (IntervalType == IntervalType.FromLastCompletion)
{
lock (Lock)
if (IntervalType == IntervalType.FromLastCompletion)
{
LastRunTime = DateTimeOffset.Now;
lock (Lock)
{
LastRunTime = DateTimeOffset.Now;
}
}
}
});
});

return true;
}
return true;
}

return false;
return false;
}
}

/// <summary>
Expand Down
Loading