diff --git a/src/Orleans.Runtime/Catalog/ActivationCollector.cs b/src/Orleans.Runtime/Catalog/ActivationCollector.cs index 4d2d89a6968..e6a1760c00d 100644 --- a/src/Orleans.Runtime/Catalog/ActivationCollector.cs +++ b/src/Orleans.Runtime/Catalog/ActivationCollector.cs @@ -362,7 +362,7 @@ internal async Task DeactivateInDueTimeOrder(int count, CancellationToken cancel var candidates = new List(count); - foreach (var bucket in buckets.OrderBy(b => b.Key)) + foreach (var bucket in buckets.ToList().OrderBy(b => b.Key)) { foreach (var item in bucket.Value.Items) { diff --git a/test/NonSilo.Tests/Runtime/ActivationCollectorTests.cs b/test/NonSilo.Tests/Runtime/ActivationCollectorTests.cs index 8aa2faa8c15..c73c66689ed 100644 --- a/test/NonSilo.Tests/Runtime/ActivationCollectorTests.cs +++ b/test/NonSilo.Tests/Runtime/ActivationCollectorTests.cs @@ -175,5 +175,66 @@ public async Task DeactivateInDueTimeOrder_OnlyOldestAndEligibleAreDeactivated() Assert.Equal(1, collector._activationCount); } + + [Fact] + public async Task DeactivateInDueTimeOrder_HandlesRaceDuringEnumeration() + { + var grainCollectionOptions = Options.Create(new GrainCollectionOptions()); + + var logger = NullLogger.Instance; + var statsProvider = Substitute.For(); + var timeProvider = new FakeTimeProvider(DateTimeOffset.UtcNow); + + var collector = new ActivationCollector(timeProvider, grainCollectionOptions, logger, statsProvider); + var timer = Substitute.For(); + timer.NextTick().Returns(Task.FromResult(false)); + var timerFactory = Substitute.For(); + timerFactory.Create(Arg.Any(), Arg.Any()).Returns(timer); + + var wsLogger = NullLogger.Instance; + var workingSet = new ActivationWorkingSet(timerFactory, wsLogger, new[] { collector }); + + var activations = new List<(ICollectibleGrainContext, IActivationWorkingSetMember)>(); + + for (int i = 0; i < 100; i++) + { + var activation = Substitute.For(); + activation.CollectionAgeLimit.Returns(TimeSpan.FromMinutes(1)); + activation.IsValid.Returns(true); + activation.IsExemptFromCollection.Returns(false); + activation.IsInactive.Returns(true); + activation.Deactivated.Returns(Task.CompletedTask).AndDoes(_ => { Interlocked.Decrement(ref collector._activationCount); }); + ((IActivationWorkingSetMember)activation).IsCandidateForRemoval(Arg.Any()).Returns(true); + + workingSet.OnActivated(activation as IActivationWorkingSetMember); + activations.Add((activation, activation as IActivationWorkingSetMember)); + } + + var deactivateTask = Task.Run(async () => + { + await collector.DeactivateInDueTimeOrder(50, CancellationToken.None); + }); + + var addRemoveTask = Task.Run(() => + { + for (int i = 0; i < 50; i++) + { + var activation = Substitute.For(); + activation.CollectionAgeLimit.Returns(TimeSpan.FromMinutes(1)); + activation.IsValid.Returns(true); + activation.IsExemptFromCollection.Returns(false); + activation.IsInactive.Returns(true); + activation.Deactivated.Returns(Task.CompletedTask).AndDoes(_ => { Interlocked.Decrement(ref collector._activationCount); }); + ((IActivationWorkingSetMember)activation).IsCandidateForRemoval(Arg.Any()).Returns(true); + + workingSet.OnActivated(activation as IActivationWorkingSetMember); + Thread.Sleep(1); + } + }); + + await Task.WhenAll(deactivateTask, addRemoveTask); + + Assert.True(collector._activationCount >= 0); + } } }