diff --git a/Directory.Packages.props b/Directory.Packages.props index efaeb772e15..0eb337a8b8d 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -45,6 +45,7 @@ + @@ -123,4 +124,4 @@ - + \ No newline at end of file diff --git a/src/Orleans.Runtime/Catalog/ActivationCollector.cs b/src/Orleans.Runtime/Catalog/ActivationCollector.cs index e2b6d679226..c2647ba120c 100644 --- a/src/Orleans.Runtime/Catalog/ActivationCollector.cs +++ b/src/Orleans.Runtime/Catalog/ActivationCollector.cs @@ -27,23 +27,21 @@ internal partial class ActivationCollector : IActivationWorkingSetObserver, ILif private Task _collectionLoopTask; private int collectionNumber; private int _activationCount; - private readonly IOptions _options; /// /// Initializes a new instance of the class. /// - /// The timer factory. + /// The time provider. /// The options. /// The logger. public ActivationCollector( - IAsyncTimerFactory timerFactory, + TimeProvider timeProvider, IOptions options, ILogger logger) { - _options = options; quantum = options.Value.CollectionQuantum; shortestAgeLimit = new(options.Value.ClassSpecificCollectionAge.Values.Aggregate(options.Value.CollectionAge.Ticks, (a, v) => Math.Min(a, v.Ticks))); - nextTicket = MakeTicketFromDateTime(DateTime.UtcNow); + nextTicket = MakeTicketFromDateTime(timeProvider.GetUtcNow().UtcDateTime); this.logger = logger; _collectionTimer = new PeriodicTimer(quantum); } @@ -325,11 +323,17 @@ private bool IsExpired(DateTime ticket) return ticket < nextTicket; } - private DateTime MakeTicketFromDateTime(DateTime timestamp) + public DateTime MakeTicketFromDateTime(DateTime timestamp) { // Round the timestamp to the next quantum. e.g. if the quantum is 1 minute and the timestamp is 3:45:22, then the ticket will be 3:46. // Note that TimeStamp.Ticks and DateTime.Ticks both return a long. - var ticket = new DateTime(((timestamp.Ticks - 1) / quantum.Ticks + 1) * quantum.Ticks, DateTimeKind.Utc); + var ticketTicks = ((timestamp.Ticks - 1) / quantum.Ticks + 1) * quantum.Ticks; + if (ticketTicks > DateTime.MaxValue.Ticks) + { + return DateTime.MaxValue; + } + + var ticket = new DateTime(ticketTicks, DateTimeKind.Utc); if (ticket < nextTicket) { throw new ArgumentException(string.Format("The earliest collection that can be scheduled from now is for {0}", new DateTime(nextTicket.Ticks - quantum.Ticks + 1, DateTimeKind.Utc))); diff --git a/test/NonSilo.Tests/NonSilo.Tests.csproj b/test/NonSilo.Tests/NonSilo.Tests.csproj index 3868e1121e7..c139dd41679 100644 --- a/test/NonSilo.Tests/NonSilo.Tests.csproj +++ b/test/NonSilo.Tests/NonSilo.Tests.csproj @@ -14,6 +14,7 @@ + diff --git a/test/NonSilo.Tests/Runtime/ActivationCollectorTests.cs b/test/NonSilo.Tests/Runtime/ActivationCollectorTests.cs new file mode 100644 index 00000000000..3524942e9fe --- /dev/null +++ b/test/NonSilo.Tests/Runtime/ActivationCollectorTests.cs @@ -0,0 +1,60 @@ +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Time.Testing; +using Orleans.Configuration; +using Xunit; + +namespace UnitTests.Runtime +{ + [TestCategory("BVT"), TestCategory("Runtime")] + public class ActivationCollectorTests + { + private readonly FakeTimeProvider timeProvider; + private readonly ActivationCollector collector; + + public ActivationCollectorTests() + { + var grainCollectionOptions = Options.Create(new GrainCollectionOptions()); + var logger = NullLogger.Instance; + + this.timeProvider = new FakeTimeProvider(DateTimeOffset.Parse("2025-01-01T00:00:00.000+00:00")); + this.collector = new ActivationCollector(timeProvider, grainCollectionOptions, logger); + } + + [Theory, TestCategory("Activation")] + [InlineData("2025-01-01T00:00:00", "2025-01-01T00:00:00")] + [InlineData("2025-01-01T00:00:01", "2025-01-01T00:01:00")] + [InlineData("2025-01-01T00:00:59", "2025-01-01T00:01:00")] + [InlineData("2025-01-01T00:01:01", "2025-01-01T00:02:00")] + public void MakeTicketFromDateTime(string timestampString, string expectedTicketString) + { + var timestamp = DateTime.Parse(timestampString); + var expectedTicket = DateTime.Parse(expectedTicketString); + + var actualTicket = collector.MakeTicketFromDateTime(timestamp); + + Assert.Equal(expectedTicket, actualTicket); + } + + [Fact, TestCategory("Activation")] + public void MakeTicketFromDateTime_MaxValue() + { + var expectedTicket = DateTime.MaxValue; + + var actualTicket = collector.MakeTicketFromDateTime(DateTime.MaxValue); + + Assert.Equal(expectedTicket, actualTicket); + } + + [Fact, TestCategory("Activation")] + public void MakeTicketFromDateTime_Invalid_BeforeNextTicket() + { + var timestamp = this.timeProvider.GetUtcNow().AddMinutes(-5).UtcDateTime; + + Assert.Throws(() => + { + var ticket = collector.MakeTicketFromDateTime(timestamp); + }); + } + } +}