diff --git a/src/Orleans.Reminders/ReminderService/LocalReminderService.cs b/src/Orleans.Reminders/ReminderService/LocalReminderService.cs index d2963134f33..a428111c62b 100644 --- a/src/Orleans.Reminders/ReminderService/LocalReminderService.cs +++ b/src/Orleans.Reminders/ReminderService/LocalReminderService.cs @@ -561,7 +561,10 @@ private async Task ReadAndReconcileRange(ISingleRange range, int rangeSerialNumb private void ReconcileTableEntry(ReminderEntry entry, long tableSequence, DateTime now, List stopTasks) { var key = new ReminderIdentity(entry.GrainId, entry.ReminderName); - var shouldLoad = IsReminderWithinLoadingWindow(entry, now, reminderOptions.ReminderLoadingWindow); + var nextTick = CalculateNextTickTime(entry, now); + // Keep distant schedules in storage; exact-due entries are intentionally skipped to simplify fake-time tests. + var isWithinLoadingWindow = nextTick <= now.AddClamped(reminderOptions.ReminderLoadingWindow); + var shouldLoad = nextTick > now && isWithinLoadingWindow; if (!localReminders.TryGetValue(key, out var localReminder)) { // Distant reminders remain exclusively in storage until a later refresh brings their next tick @@ -592,7 +595,7 @@ private void ReconcileTableEntry(ReminderEntry entry, long tableSequence, DateTi return; } - if (!shouldLoad) + if (!isWithinLoadingWindow || (state is LocalReminderState.Tombstone && !shouldLoad)) { LogTraceRemovingReminder(localReminder); stopTasks.Add( diff --git a/src/Orleans.Testing.Reminders/ReminderTestClock.cs b/src/Orleans.Testing.Reminders/ReminderTestClock.cs index 1fec61d77c0..d125a3f0edf 100644 --- a/src/Orleans.Testing.Reminders/ReminderTestClock.cs +++ b/src/Orleans.Testing.Reminders/ReminderTestClock.cs @@ -1,9 +1,9 @@ #nullable enable using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Time.Testing; using Orleans.Hosting; +using Orleans.Reminders; using Orleans.TestingHost; namespace Orleans.Testing.Reminders; @@ -13,8 +13,8 @@ namespace Orleans.Testing.Reminders; /// /// /// Attach an instance to an before building the cluster. -/// The attached clock replaces the silo and tunes -/// for deterministic reminder scheduling. +/// The attached clock replaces the reminder subsystem's keyed and tunes +/// for deterministic reminder scheduling without affecting unrelated silo timers. /// public sealed class ReminderTestClock : IDisposable { @@ -74,7 +74,7 @@ public static ReminderTestClock Attach( builder.ConfigureSilo((_, siloBuilder) => { - siloBuilder.Services.Replace(ServiceDescriptor.Singleton(clock.TimeProvider)); + siloBuilder.Services.AddKeyedSingleton(ReminderTimeProviderNames.Reminders, clock.TimeProvider); siloBuilder.Services.PostConfigure(options => { options.MinimumReminderPeriod = clock.MinimumReminderPeriod; diff --git a/test/Orleans.Reminders.Tests/TimerTests/ReminderTests_TableGrain.cs b/test/Orleans.Reminders.Tests/TimerTests/ReminderTests_TableGrain.cs index c275331404e..98e3be3627a 100644 --- a/test/Orleans.Reminders.Tests/TimerTests/ReminderTests_TableGrain.cs +++ b/test/Orleans.Reminders.Tests/TimerTests/ReminderTests_TableGrain.cs @@ -6,6 +6,7 @@ using System.Reactive.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Orleans.Internal; +using Orleans.Reminders; using Orleans.Testing.Reminders; using Orleans.TestingHost; using TestExtensions; @@ -36,6 +37,8 @@ public class Fixture : BaseInProcessTestClusterFixture protected override void ConfigureTestCluster(InProcessTestClusterBuilder builder) { + // The controlled-read tests validate ordering within one reminder owner. + builder.Options.InitialSilosCount = 1; _reminderClock = builder.AddReminderTestClock(); builder.ConfigureSilo((_, siloBuilder) => { @@ -78,6 +81,19 @@ public ReminderTests_TableGrain(Fixture fixture) : base(fixture.ReminderClock, f // Basic tests + [Fact] + public void ReminderTestClock_IsScopedToReminderService() + { + foreach (var silo in HostedCluster.Silos) + { + var unkeyedProvider = silo.ServiceProvider.GetRequiredService(); + var reminderProvider = silo.ServiceProvider.GetRequiredKeyedService(ReminderTimeProviderNames.Reminders); + + Assert.Same(TimeProvider.System, unkeyedProvider); + Assert.NotSame(unkeyedProvider, reminderProvider); + } + } + /// /// Tests basic reminder operations including stopping reminders by reference. ///