Skip to content

Commit cc6d029

Browse files
authored
fix: Fixes timer Delay/Next not handling MinValue inputs (#1985)
### Summary Fixes a few minor issues with timers: - Timer.Delay and Timer.Interval was not reflecting the actual tick time (aligned to the next 8ms) - Negative delay values were causing a crash when DateTime.Now - delay was below DateTime.MinValue - Timer.Next now reflects the correct wall clock tick time based on the adjusted Delay. - Timer.Next is not assigned when the timer is started. This was important for timers that were created, but started later.
1 parent ac06a0d commit cc6d029

File tree

3 files changed

+118
-94
lines changed

3 files changed

+118
-94
lines changed

Projects/Server.Tests/Tests/Timer/TimerTests.cs

Lines changed: 76 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,108 +1,108 @@
11
using System;
22
using Xunit;
33

4-
namespace Server.Tests
4+
namespace Server.Tests;
5+
6+
[Collection("Sequential Tests")]
7+
public class TimerTests : IClassFixture<ServerFixture>
58
{
6-
[Collection("Sequential Tests")]
7-
public class TimerTests : IClassFixture<ServerFixture>
9+
[Theory]
10+
[InlineData(0L, 8L)]
11+
[InlineData(80L, 80L)]
12+
[InlineData(65L, 72L)]
13+
[InlineData(32767L, 32768L)]
14+
[InlineData(32768L, 32768L)]
15+
[InlineData(32833L, 32840L)]
16+
[InlineData(134217729L, 134217736L)]
17+
public void TestVariousTimes(long ticks, long expectedTicks)
818
{
9-
[Theory]
10-
[InlineData(0L, 8L)]
11-
[InlineData(80L, 80L)]
12-
[InlineData(65L, 72L)]
13-
[InlineData(32767L, 32768L)]
14-
[InlineData(32768L, 32768L)]
15-
[InlineData(32833L, 32840L)]
16-
[InlineData(134217729L, 134217736L)]
17-
public void TestVariousTimes(long ticks, long expectedTicks)
18-
{
19-
var timerTicks = new TimerTicks();
20-
void action()
21-
{
22-
Assert.Equal(expectedTicks, timerTicks.Ticks);
23-
timerTicks.ExecutedCount++;
24-
}
19+
var timerTicks = new TimerTicks();
20+
Timer.Init(timerTicks.Ticks);
2521

26-
Timer.Init(timerTicks.Ticks);
22+
Timer.StartTimer(TimeSpan.FromMilliseconds(ticks), action);
2723

28-
Timer.StartTimer(TimeSpan.FromMilliseconds(ticks), action);
24+
var tickCount = expectedTicks / 8;
2925

30-
var tickCount = expectedTicks / 8;
26+
for (int i = 1; i <= tickCount; i++)
27+
{
28+
timerTicks.Ticks = i * 8;
3129

32-
for (int i = 1; i <= tickCount; i++)
33-
{
34-
timerTicks.Ticks = i * 8;
30+
Timer.Slice(timerTicks.Ticks);
31+
}
3532

36-
Timer.Slice(timerTicks.Ticks);
37-
}
33+
Assert.Equal(1, timerTicks.ExecutedCount);
34+
return;
3835

39-
Assert.Equal(1, timerTicks.ExecutedCount);
36+
void action()
37+
{
38+
Assert.Equal(expectedTicks, timerTicks.Ticks);
39+
timerTicks.ExecutedCount++;
4040
}
41+
}
4142

42-
[Theory]
43-
[InlineData(1000L, 1000L, 30000L, 30000L, 2)]
44-
public void TestIntervals(long delay, long expectedDelayTicks, long interval, long expectedIntervalTicks, int count)
45-
{
46-
var timerTicks = new TimerTicks();
43+
[Theory]
44+
[InlineData(1000L, 1000L, 30000L, 30000L, 2)]
45+
public void TestIntervals(long delay, long expectedDelayTicks, long interval, long expectedIntervalTicks, int count)
46+
{
47+
var timerTicks = new TimerTicks();
4748

48-
Timer.Init(timerTicks.Ticks);
49+
Timer.Init(timerTicks.Ticks);
4950

50-
Timer.StartTimer(TimeSpan.FromMilliseconds(delay), TimeSpan.FromMilliseconds(interval), count, action);
51+
Timer.StartTimer(TimeSpan.FromMilliseconds(delay), TimeSpan.FromMilliseconds(interval), count, action);
5152

52-
var tickCount = (expectedDelayTicks + (expectedIntervalTicks * count - 1)) / 8;
53+
var tickCount = (expectedDelayTicks + (expectedIntervalTicks * count - 1)) / 8;
5354

54-
for (int i = 1; i <= tickCount; i++)
55-
{
56-
timerTicks.Ticks = i * 8;
55+
for (int i = 1; i <= tickCount; i++)
56+
{
57+
timerTicks.Ticks = i * 8;
5758

58-
Timer.Slice(timerTicks.Ticks);
59-
}
59+
Timer.Slice(timerTicks.Ticks);
60+
}
6061

61-
Assert.Equal(count, timerTicks.ExecutedCount);
62-
return;
62+
Assert.Equal(count, timerTicks.ExecutedCount);
63+
return;
6364

64-
void action()
65-
{
66-
timerTicks.ExpectedTicks += timerTicks.ExecutedCount++ == 0 ? expectedDelayTicks : expectedIntervalTicks;
67-
Assert.Equal(timerTicks.ExpectedTicks, timerTicks.Ticks);
68-
}
65+
void action()
66+
{
67+
timerTicks.ExpectedTicks += timerTicks.ExecutedCount++ == 0 ? expectedDelayTicks : expectedIntervalTicks;
68+
Assert.Equal(timerTicks.ExpectedTicks, timerTicks.Ticks);
6969
}
70+
}
7071

71-
[Fact]
72-
public void TestTimerStartedOnTick()
73-
{
74-
var timerTicks = new TimerTicks();
72+
[Fact]
73+
public void TestTimerStartedOnTick()
74+
{
75+
var timerTicks = new TimerTicks();
7576

76-
Timer.Init(timerTicks.Ticks);
77+
Timer.Init(timerTicks.Ticks);
7778

78-
var timer = new SelfRunningTimer(timerTicks);
79-
timer.Start();
79+
var timer = new SelfRunningTimer(timerTicks);
80+
timer.Start();
8081

81-
Timer.Slice(128);
82-
Assert.Equal(1, timerTicks.ExecutedCount);
83-
Timer.Slice(256);
84-
Assert.Equal(2, timerTicks.ExecutedCount);
85-
}
82+
Timer.Slice(128);
83+
Assert.Equal(1, timerTicks.ExecutedCount);
84+
Timer.Slice(256);
85+
Assert.Equal(2, timerTicks.ExecutedCount);
86+
}
8687

87-
private class TimerTicks
88-
{
89-
public long ExpectedTicks;
90-
public long Ticks;
91-
public int ExecutedCount;
92-
}
88+
private class TimerTicks
89+
{
90+
public long ExpectedTicks;
91+
public long Ticks;
92+
public int ExecutedCount;
93+
}
9394

94-
private class SelfRunningTimer : Timer
95-
{
96-
private readonly TimerTicks _timerTicks;
97-
public SelfRunningTimer(TimerTicks ticks) : base(TimeSpan.FromMilliseconds(100)) => _timerTicks = ticks;
95+
private class SelfRunningTimer : Timer
96+
{
97+
private readonly TimerTicks _timerTicks;
98+
public SelfRunningTimer(TimerTicks ticks) : base(TimeSpan.FromMilliseconds(100)) => _timerTicks = ticks;
9899

99-
protected override void OnTick()
100+
protected override void OnTick()
101+
{
102+
if (_timerTicks.ExecutedCount++ == 0)
100103
{
101-
if (_timerTicks.ExecutedCount++ == 0)
102-
{
103-
Delay = TimeSpan.FromMilliseconds(100);
104-
Start();
105-
}
104+
Delay = TimeSpan.FromMilliseconds(100);
105+
Start();
106106
}
107107
}
108108
}

Projects/Server/Timer/Timer.TimerWheel.cs

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/*************************************************************************
22
* ModernUO *
3-
* Copyright 2019-2023 - ModernUO Development Team *
3+
* Copyright 2019-2024 - ModernUO Development Team *
44
* Email: [email protected] *
55
* File: Timer.TimerWheel.cs *
66
* *
@@ -18,6 +18,7 @@
1818
using System.Diagnostics;
1919
using System.IO;
2020
using System.Linq;
21+
using System.Runtime.CompilerServices;
2122

2223
namespace Server;
2324

@@ -33,9 +34,9 @@ public partial class Timer
3334
private const int _tickRate = 1 << _tickRatePowerOf2; // 8ms
3435
private const long _maxDuration = (long)_tickRate << (_ringSizePowerOf2 * _ringLayers - 1);
3536

36-
private static Timer[][] _rings = new Timer[_ringLayers][];
37-
private static int[] _ringIndexes = new int[_ringLayers];
38-
private static Timer[] _executingRings = new Timer[_ringLayers];
37+
private static readonly Timer[][] _rings = new Timer[_ringLayers][];
38+
private static readonly int[] _ringIndexes = new int[_ringLayers];
39+
private static readonly Timer[] _executingRings = new Timer[_ringLayers];
3940

4041
private static long _lastTickTurned = -1;
4142

@@ -155,9 +156,7 @@ private static void Execute(Timer timer)
155156

156157
if (!finished)
157158
{
158-
timer.Delay = timer.Interval;
159-
timer.Next = DateTime.UtcNow + timer.Interval;
160-
AddTimer(timer, (long)timer.Delay.TotalMilliseconds);
159+
AddTimer(timer, (long)timer.Interval.TotalMilliseconds);
161160
}
162161
else
163162
{
@@ -168,10 +167,21 @@ private static void Execute(Timer timer)
168167
timer.Index++;
169168
}
170169

170+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
171+
private static long RoundTicksToNextPowerOfTwo(long value)
172+
{
173+
if (value <= 0)
174+
{
175+
return _tickRate;
176+
}
177+
178+
const long mask = _tickRate - 1;
179+
return (value + mask) & ~mask;
180+
}
181+
171182
private static void AddTimer(Timer timer, long delay)
172183
{
173-
var originalDelay = delay;
174-
delay = Math.Max(0, delay);
184+
var actualDelay = delay;
175185

176186
var resolutionPowerOf2 = _tickRatePowerOf2;
177187
for (var i = 0; i < _ringLayers; i++)
@@ -205,7 +215,7 @@ private static void AddTimer(Timer timer, long delay)
205215
logger.Error(
206216
$"Timer {{Timer}} has a duration of {{Duration}}ms, more than max capacity of {{MaxDuration}}ms.{Environment.NewLine}{{StackTrace}}",
207217
timer.GetType(),
208-
originalDelay,
218+
actualDelay,
209219
_maxDuration,
210220
new StackTrace()
211221
);
@@ -214,18 +224,19 @@ private static void AddTimer(Timer timer, long delay)
214224
}
215225
}
216226

227+
timer.Next = Core.Now + timer.Delay;
217228
timer.Attach(_rings[i][slot]);
218229
timer._remaining = remaining;
219230
timer._ring = i;
220231
timer._slot = (int)slot;
221232

222233
_rings[i][slot] = timer;
223-
224234
return;
225235
}
226236

227237
// The remaining amount until we turn this ring
228-
delay -= resolution * (_ringSize - _ringIndexes[i]);
238+
var offsetDelay = resolution * (_ringSize - _ringIndexes[i]);
239+
delay -= offsetDelay;
229240
resolutionPowerOf2 = nextResolutionPowerOf2;
230241
}
231242
}

Projects/Server/Timer/Timer.cs

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/*************************************************************************
22
* ModernUO *
3-
* Copyright 2019-2023 - ModernUO Development Team *
3+
* Copyright 2019-2024 - ModernUO Development Team *
44
* Email: [email protected] *
55
* File: Timer.cs *
66
* *
@@ -34,6 +34,8 @@ public static void Configure()
3434
private long _remaining;
3535
private Timer _nextTimer;
3636
private Timer _prevTimer;
37+
private TimeSpan _delay;
38+
private TimeSpan _interval;
3739

3840
public Timer(TimeSpan delay) => Init(delay, TimeSpan.Zero, 1);
3941

@@ -43,23 +45,34 @@ public static void Configure()
4345

4446
protected void Init(TimeSpan delay, TimeSpan interval, int count)
4547
{
46-
Running = false;
4748
Delay = delay;
48-
Index = 0;
49+
Next = DateTime.MinValue;
4950
Interval = interval;
5051
Count = count;
52+
Running = false;
53+
Index = 0;
5154
_nextTimer = null;
5255
_prevTimer = null;
53-
Next = Core.Now + Delay;
5456
_ring = -1;
5557
_slot = -1;
5658
}
5759

5860
protected int Version { get; set; } // Used to determine if a timer was altered and we should abandon it.
5961

6062
public DateTime Next { get; private set; }
61-
public TimeSpan Delay { get; set; }
62-
public TimeSpan Interval { get; set; }
63+
64+
public TimeSpan Delay
65+
{
66+
get => _delay;
67+
set => _delay = TimeSpan.FromMilliseconds(RoundTicksToNextPowerOfTwo((long)value.TotalMilliseconds));
68+
}
69+
70+
public TimeSpan Interval
71+
{
72+
get => _interval;
73+
set => _interval = TimeSpan.FromMilliseconds(RoundTicksToNextPowerOfTwo((long)value.TotalMilliseconds));
74+
}
75+
6376
public int Index { get; private set; }
6477
public int Count { get; private set; }
6578
public int RemainingCount => Count == 0 ? int.MaxValue : Count - Index;

0 commit comments

Comments
 (0)