diff --git a/playground/ActivationRepartitioning/ActivationRepartitioning.Frontend/Program.cs b/playground/ActivationRepartitioning/ActivationRepartitioning.Frontend/Program.cs index 51076b56a80..5a51e3f8de7 100644 --- a/playground/ActivationRepartitioning/ActivationRepartitioning.Frontend/Program.cs +++ b/playground/ActivationRepartitioning/ActivationRepartitioning.Frontend/Program.cs @@ -97,7 +97,7 @@ public async ValueTask Reset() { ++_resetCount; _numForests = 0; - await ServiceProvider.GetRequiredService().ResetAsync(); + await ServiceProvider!.GetRequiredService().ResetAsync(); await GrainFactory.GetGrain(0).ResetGrainCallFrequencies(); } diff --git a/src/Orleans.Core.Abstractions/Core/Grain.cs b/src/Orleans.Core.Abstractions/Core/Grain.cs index 7fcb8deb06d..ff299881763 100644 --- a/src/Orleans.Core.Abstractions/Core/Grain.cs +++ b/src/Orleans.Core.Abstractions/Core/Grain.cs @@ -1,5 +1,6 @@ using System; using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; @@ -14,6 +15,8 @@ namespace Orleans; /// public abstract partial class Grain : IGrainBase, IAddressable { + private static readonly ConditionalWeakTable StandaloneRuntimes = new(); + // Do not use this directly because we currently don't provide a way to inject it; // any interaction with it will result in non unit-testable code. Any behavior that can be accessed // from within client code (including subclasses of this class), should be exposed through IGrainRuntime. @@ -23,18 +26,24 @@ public abstract partial class Grain : IGrainBase, IAddressable public GrainReference GrainReference { get { return GrainContext.GrainReference; } } - internal IGrainRuntime Runtime { get; } + private IGrainRuntime? RuntimeOrDefault => GrainContext?.GrainRuntime + ?? (StandaloneRuntimes.TryGetValue(this, out var runtime) ? runtime : null); + + internal IGrainRuntime Runtime => RuntimeOrDefault + ?? throw new InvalidOperationException("Grain was created outside of the Orleans creation process and no runtime was specified."); /// - /// Gets an object which can be used to access other grains. Null if this grain is not associated with a Runtime, such as when created directly for unit testing. + /// Gets an object which can be used to access other grains. /// + /// + /// The grain was created outside of the Orleans activation process and no runtime was provided. + /// protected IGrainFactory GrainFactory => Runtime.GrainFactory; /// /// Gets the IServiceProvider managed by the runtime. Null if this grain is not associated with a Runtime, such as when created directly for unit testing. /// - // ! The runtime ensures that this is not null and Unit testing frameworks must make sure that this is not null. - protected internal IServiceProvider ServiceProvider => GrainContext?.ActivationServices ?? Runtime?.ServiceProvider!; + protected internal IServiceProvider? ServiceProvider => GrainContext?.ActivationServices ?? RuntimeOrDefault?.ServiceProvider; internal GrainId GrainId => GrainContext.GrainId; @@ -51,12 +60,24 @@ protected Grain() : this(RuntimeContext.Current!, grainRuntime: null) /// This constructor is particularly useful for unit testing where test code can create a Grain and replace /// the IGrainIdentity and IGrainRuntime with test doubles (mocks/stubs). /// + /// + /// When is provided, it is associated with this grain and registered as an + /// component when is available. + /// protected Grain(IGrainContext grainContext, IGrainRuntime? grainRuntime = null) { GrainContext = grainContext; - - // ! The runtime ensures that this is not null and Unit testing frameworks must make sure that this is not null. - Runtime = grainRuntime ?? grainContext?.ActivationServices.GetService()!; + if (grainRuntime is not null) + { + if (grainContext is null) + { + StandaloneRuntimes.Add(this, grainRuntime); + } + else + { + grainContext.GrainRuntime = grainRuntime; + } + } } /// @@ -68,7 +89,7 @@ protected Grain(IGrainContext grainContext, IGrainRuntime? grainRuntime = null) /// A unique identifier for the current silo. /// There is no semantic content to this string, but it may be useful for logging. /// - public string RuntimeIdentity => Runtime?.SiloIdentity ?? string.Empty; + public string RuntimeIdentity => RuntimeOrDefault?.SiloIdentity ?? string.Empty; /// /// Registers a timer to send periodic callbacks to this grain. @@ -163,13 +184,7 @@ protected void DelayDeactivation(TimeSpan timeSpan) /// A cancellation token which signals when deactivation should complete promptly. public virtual Task OnDeactivateAsync(DeactivationReason reason, CancellationToken cancellationToken) => Task.CompletedTask; - internal void EnsureRuntime() - { - if (Runtime == null) - { - throw new InvalidOperationException("Grain was created outside of the Orleans creation process and no runtime was specified."); - } - } + internal void EnsureRuntime() => _ = Runtime; } /// diff --git a/src/Orleans.Core.Abstractions/Core/IGrainContext.cs b/src/Orleans.Core.Abstractions/Core/IGrainContext.cs index 3e78f7435f2..cb2affc0d67 100644 --- a/src/Orleans.Core.Abstractions/Core/IGrainContext.cs +++ b/src/Orleans.Core.Abstractions/Core/IGrainContext.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; using Orleans.Serialization.Invocation; namespace Orleans.Runtime @@ -41,6 +42,20 @@ public interface IGrainContext : ITargetHolder, IEquatable /// IServiceProvider ActivationServices { get; } + /// + /// Gets or sets the grain runtime associated with this context. + /// + /// + /// Runtime contexts provide the activation runtime directly. Custom contexts resolve an + /// component before resolving an activation service. + /// + IGrainRuntime? GrainRuntime + { + get => GetComponent(typeof(IGrainRuntime)) as IGrainRuntime + ?? ActivationServices?.GetService(); + set => SetComponent(value); + } + /// /// Gets the observable lifecycle, which can be used to add lifecycle hooks. /// diff --git a/src/Orleans.EventSourcing/LogConsistency/LogConsistentGrain.cs b/src/Orleans.EventSourcing/LogConsistency/LogConsistentGrain.cs index 7b5cdb617f4..f04b21adf87 100644 --- a/src/Orleans.EventSourcing/LogConsistency/LogConsistentGrain.cs +++ b/src/Orleans.EventSourcing/LogConsistency/LogConsistentGrain.cs @@ -55,11 +55,12 @@ private async Task OnDeactivateState(CancellationToken ct) private Task OnSetupState(CancellationToken ct) { if (ct.IsCancellationRequested) return Task.CompletedTask; - IGrainContextAccessor grainContextAccessor = this.ServiceProvider.GetRequiredService(); - Factory protocolServicesFactory = this.ServiceProvider.GetRequiredService>(); + var serviceProvider = ServiceProvider!; + IGrainContextAccessor grainContextAccessor = serviceProvider.GetRequiredService(); + Factory protocolServicesFactory = serviceProvider.GetRequiredService>(); IGrainContext grainContext = grainContextAccessor.GrainContext!; ILogViewAdaptorFactory consistencyProvider = SetupLogConsistencyProvider(grainContext); - IGrainStorage? grainStorage = consistencyProvider.UsesStorageProvider ? GrainStorageHelpers.GetGrainStorage((grainContext?.GrainInstance!.GetType())!, this.ServiceProvider) : null; + IGrainStorage? grainStorage = consistencyProvider.UsesStorageProvider ? GrainStorageHelpers.GetGrainStorage((grainContext?.GrainInstance!.GetType())!, serviceProvider) : null; InstallLogViewAdaptor(grainContext!, protocolServicesFactory, consistencyProvider, grainStorage); return Task.CompletedTask; } @@ -91,10 +92,11 @@ private void InstallLogViewAdaptor( private ILogViewAdaptorFactory SetupLogConsistencyProvider(IGrainContext activationContext) { var attr = this.GetType().GetCustomAttributes(true).FirstOrDefault(); + var serviceProvider = ServiceProvider!; ILogViewAdaptorFactory? defaultFactory = attr != null - ? this.ServiceProvider.GetKeyedService(attr.ProviderName) - : this.ServiceProvider.GetService(); + ? serviceProvider.GetKeyedService(attr.ProviderName) + : serviceProvider.GetService(); if (attr != null && defaultFactory == null) { var errMsg = $"Cannot find consistency provider with Name={attr.ProviderName} for grain type {this.GetType().FullName}"; diff --git a/src/Orleans.Journaling/DurableGrain.cs b/src/Orleans.Journaling/DurableGrain.cs index 7ffec5d7c2a..b54ef738ffb 100644 --- a/src/Orleans.Journaling/DurableGrain.cs +++ b/src/Orleans.Journaling/DurableGrain.cs @@ -6,7 +6,7 @@ public abstract class DurableGrain : Grain, IGrainBase { protected DurableGrain() { - StateManager = ServiceProvider.GetRequiredService(); + StateManager = ServiceProvider!.GetRequiredService(); if (StateManager is ILifecycleParticipant participant) { participant.Participate(((IGrainBase)this).GrainContext.ObservableLifecycle); @@ -16,7 +16,7 @@ protected DurableGrain() protected IJournaledStateManager StateManager { get; } protected TState GetOrCreateState(string name) where TState : class, IJournaledState - => GetOrCreateState(name, static sp => sp.GetRequiredService(), ServiceProvider); + => GetOrCreateState(name, static sp => sp.GetRequiredService(), ServiceProvider!); protected TState GetOrCreateState(string name, Func createState, TArg arg) where TState : class, IJournaledState { diff --git a/src/Orleans.Runtime/Catalog/ActivationData.cs b/src/Orleans.Runtime/Catalog/ActivationData.cs index 0ce9b85710e..d64a2a50283 100644 --- a/src/Orleans.Runtime/Catalog/ActivationData.cs +++ b/src/Orleans.Runtime/Catalog/ActivationData.cs @@ -142,7 +142,20 @@ public void Start(IGrainActivator grainActivator) } public ActivationTaskScheduler ActivationTaskScheduler => _workItemGroup.TaskScheduler; - public IGrainRuntime GrainRuntime => _shared.Runtime; + public IGrainRuntime GrainRuntime + { + get + { + var extras = Volatile.Read(ref _extras); + return extras is null ? _shared.Runtime : Volatile.Read(ref extras.GrainRuntime) ?? _shared.Runtime; + } + } + + IGrainRuntime? IGrainContext.GrainRuntime + { + get => GrainRuntime; + set => SetComponent(value); + } public object? GrainInstance { get; private set; } public GrainAddress Address { get; private set; } public GrainReference GrainReference => _selfReference ??= _shared.GrainReferenceActivator.CreateReference(GrainId, default); @@ -327,6 +340,10 @@ private DehydrationContextHolder? DehydrationContext { result = this; } + else if (componentType == typeof(IGrainRuntime)) + { + result = GrainRuntime; + } else if (_extras is { } components && components.TryGetValue(componentType, out var resultObj)) { result = resultObj; @@ -363,6 +380,25 @@ public void SetComponent(Type componentType, object? instance) lock (this) { + if (componentType == typeof(IGrainRuntime)) + { + var extras = Volatile.Read(ref _extras); + if (extras is null) + { + if (instance is null) + { + return; + } + + extras = new() { GrainRuntime = (IGrainRuntime)instance }; + Volatile.Write(ref _extras, extras); + return; + } + + Volatile.Write(ref extras.GrainRuntime, (IGrainRuntime?)instance); + return; + } + if (instance == null) { _extras?.Remove(componentType); @@ -2318,6 +2354,8 @@ private class ActivationDataExtra : Dictionary private const int IsDisposingFlag = 1 << 2; private byte _flags; + public IGrainRuntime? GrainRuntime; + public HashSet? Timers { get => GetValueOrDefault>(nameof(Timers)); set => SetOrRemoveValue(nameof(Timers), value); } /// diff --git a/src/Orleans.Runtime/Catalog/StatelessWorkerGrainContext.cs b/src/Orleans.Runtime/Catalog/StatelessWorkerGrainContext.cs index b244328caf0..bfc8d1fed38 100644 --- a/src/Orleans.Runtime/Catalog/StatelessWorkerGrainContext.cs +++ b/src/Orleans.Runtime/Catalog/StatelessWorkerGrainContext.cs @@ -77,6 +77,20 @@ public StatelessWorkerGrainContext( public GrainAddress Address { get; } + public IGrainRuntime GrainRuntime => _shared.Shared.Runtime; + + IGrainRuntime? IGrainContext.GrainRuntime + { + get => GrainRuntime; + set + { + if (!ReferenceEquals(value, GrainRuntime)) + { + throw new ArgumentException("The runtime for a stateless worker context is provided by its shared grain type context.", nameof(value)); + } + } + } + public IServiceProvider ActivationServices => throw new NotImplementedException(); public IGrainLifecycle ObservableLifecycle => throw new NotImplementedException(); diff --git a/src/Orleans.TestingHost/TestStorageProviders/StorageFaultGrain.cs b/src/Orleans.TestingHost/TestStorageProviders/StorageFaultGrain.cs index 9c512aeb969..860676facb4 100644 --- a/src/Orleans.TestingHost/TestStorageProviders/StorageFaultGrain.cs +++ b/src/Orleans.TestingHost/TestStorageProviders/StorageFaultGrain.cs @@ -23,7 +23,7 @@ public partial class StorageFaultGrain : Grain, IStorageFaultGrain public override async Task OnActivateAsync(CancellationToken cancellationToken) { await base.OnActivateAsync(cancellationToken); - logger = this.ServiceProvider.GetService()!.CreateLogger($"{typeof (StorageFaultGrain).FullName}-{IdentityString}-{RuntimeIdentity}"); + logger = ServiceProvider!.GetService()!.CreateLogger($"{typeof (StorageFaultGrain).FullName}-{IdentityString}-{RuntimeIdentity}"); readFaults = new(); writeFaults = new(); clearfaults = new(); diff --git a/src/api/Orleans.Core.Abstractions/Orleans.Core.Abstractions.cs b/src/api/Orleans.Core.Abstractions/Orleans.Core.Abstractions.cs index bd4ddd6a280..a4d5d336034 100644 --- a/src/api/Orleans.Core.Abstractions/Orleans.Core.Abstractions.cs +++ b/src/api/Orleans.Core.Abstractions/Orleans.Core.Abstractions.cs @@ -1071,7 +1071,7 @@ protected Grain(Runtime.IGrainContext grainContext, Runtime.IGrainRuntime? grain public string RuntimeIdentity { get { throw null; } } - protected internal System.IServiceProvider ServiceProvider { get { throw null; } } + protected internal System.IServiceProvider? ServiceProvider { get { throw null; } } protected void DeactivateOnIdle() { } @@ -2555,6 +2555,8 @@ public partial interface IGrainContext : Orleans.Serialization.Invocation.ITarge GrainReference GrainReference { get; } + IGrainRuntime? GrainRuntime { get; set; } + IGrainLifecycle ObservableLifecycle { get; } IWorkItemScheduler Scheduler { get; } diff --git a/test/Grains/TestGrains/MessageSerializationGrain.cs b/test/Grains/TestGrains/MessageSerializationGrain.cs index 50342e7cac0..69b746c8872 100644 --- a/test/Grains/TestGrains/MessageSerializationGrain.cs +++ b/test/Grains/TestGrains/MessageSerializationGrain.cs @@ -59,8 +59,9 @@ private async Task GetGrainOnOtherSilo() IMessageSerializationGrain otherGrain; var id = this.GetPrimaryKeyLong(); var currentSiloIdentity = await this.GetSiloIdentity(); - var silos = ServiceProvider.GetRequiredService().CurrentSnapshot.Members.Where(kv => kv.Value.Status == SiloStatus.Active).Select(kv => kv.Key).ToHashSet(); - silos.Remove(ServiceProvider.GetRequiredService().SiloAddress); + var serviceProvider = ServiceProvider!; + var silos = serviceProvider.GetRequiredService().CurrentSnapshot.Members.Where(kv => kv.Value.Status == SiloStatus.Active).Select(kv => kv.Key).ToHashSet(); + silos.Remove(serviceProvider.GetRequiredService().SiloAddress); while (true) { RequestContext.Set(IPlacementDirector.PlacementHintKey, silos.First()); diff --git a/test/Grains/TestGrains/ProgrammaticSubscribe/SubscribeGrain.cs b/test/Grains/TestGrains/ProgrammaticSubscribe/SubscribeGrain.cs index e56dfc0a2fd..680199aa6b6 100644 --- a/test/Grains/TestGrains/ProgrammaticSubscribe/SubscribeGrain.cs +++ b/test/Grains/TestGrains/ProgrammaticSubscribe/SubscribeGrain.cs @@ -14,7 +14,7 @@ public class SubscribeGrain : Grain, ISubscribeGrain { public Task CanGetSubscriptionManager(string providerName) { - return Task.FromResult(this.ServiceProvider.GetKeyedService(providerName)!.TryGetStreamSubscriptionManager(out _)); // The test configures the named provider before invoking this grain. + return Task.FromResult(ServiceProvider!.GetKeyedService(providerName)!.TryGetStreamSubscriptionManager(out _)); // The test configures the named provider before invoking this grain. } } diff --git a/test/Grains/TestGrains/SimpleDIGrain.cs b/test/Grains/TestGrains/SimpleDIGrain.cs index 060f012f75f..5768caf5ddc 100644 --- a/test/Grains/TestGrains/SimpleDIGrain.cs +++ b/test/Grains/TestGrains/SimpleDIGrain.cs @@ -65,9 +65,10 @@ public Task DoDeactivate() public Task AssertCanResolveSameServiceInstances() { - if (!ReferenceEquals(this.ServiceProvider.GetRequiredService(), this.injectedService)) throw new Exception("singleton not equal"); - if (!ReferenceEquals(this.ServiceProvider.GetRequiredService(), this.injectedScopedService)) throw new Exception("scoped not equal"); - if (!ReferenceEquals(this.ServiceProvider.GetRequiredService().GrainContext, this.originalGrainContext)) throw new Exception("scoped grain activation context not equal"); + var serviceProvider = ServiceProvider!; + if (!ReferenceEquals(serviceProvider.GetRequiredService(), this.injectedService)) throw new Exception("singleton not equal"); + if (!ReferenceEquals(serviceProvider.GetRequiredService(), this.injectedScopedService)) throw new Exception("scoped not equal"); + if (!ReferenceEquals(serviceProvider.GetRequiredService().GrainContext, this.originalGrainContext)) throw new Exception("scoped grain activation context not equal"); return Task.CompletedTask; } diff --git a/test/Grains/TestInternalGrains/CollectionTestGrain.cs b/test/Grains/TestInternalGrains/CollectionTestGrain.cs index 7e7b573a8c2..a8c996a603d 100644 --- a/test/Grains/TestInternalGrains/CollectionTestGrain.cs +++ b/test/Grains/TestInternalGrains/CollectionTestGrain.cs @@ -29,7 +29,7 @@ protected virtual ILogger Logger() public override Task OnActivateAsync(CancellationToken cancellationToken) { - logger = this.ServiceProvider.GetRequiredService() + logger = ServiceProvider!.GetRequiredService() .CreateLogger(string.Format("CollectionTestGrain {0} {1} on {2}.", GrainId, _grainContext.ActivationId, RuntimeIdentity)); logger.LogInformation("OnActivateAsync."); activated = DateTime.UtcNow; @@ -127,7 +127,7 @@ protected override ILogger Logger() public override Task OnActivateAsync(CancellationToken cancellationToken) { - logger = this.ServiceProvider.GetRequiredService() + logger = ServiceProvider!.GetRequiredService() .CreateLogger($"CollectionTestGrain {GrainId} {_grainContext.ActivationId} on {RuntimeIdentity}."); logger.LogInformation("OnActivateAsync."); counter = 0; diff --git a/test/Grains/TestInternalGrains/PersistenceTestGrains.cs b/test/Grains/TestInternalGrains/PersistenceTestGrains.cs index 0744ba0d0e0..6c0e64189e8 100644 --- a/test/Grains/TestInternalGrains/PersistenceTestGrains.cs +++ b/test/Grains/TestInternalGrains/PersistenceTestGrains.cs @@ -67,7 +67,7 @@ public Task CheckStateInit() public Task CheckProviderType() { - IGrainStorage grainStorage = GrainStorageHelpers.GetGrainStorage(GetType(), this.ServiceProvider); + IGrainStorage grainStorage = GrainStorageHelpers.GetGrainStorage(GetType(), ServiceProvider!); Assert.NotNull(grainStorage); return Task.FromResult(grainStorage.GetType().FullName!); } diff --git a/test/Grains/TestInternalGrains/PlacementTestGrain.cs b/test/Grains/TestInternalGrains/PlacementTestGrain.cs index f8cfd8da9cf..6b8f13edcb0 100644 --- a/test/Grains/TestInternalGrains/PlacementTestGrain.cs +++ b/test/Grains/TestInternalGrains/PlacementTestGrain.cs @@ -252,7 +252,7 @@ internal class DefaultPlacementGrain : Grain, IDefaultPlacementGrain { public Task GetDefaultPlacement() { - var defaultStrategy = this.ServiceProvider.GetRequiredService(); + var defaultStrategy = ServiceProvider!.GetRequiredService(); return Task.FromResult(defaultStrategy); } } diff --git a/test/Grains/TestInternalGrains/SerializerPresenceTestGrain.cs b/test/Grains/TestInternalGrains/SerializerPresenceTestGrain.cs index 3aedb9058fa..4a47e836def 100644 --- a/test/Grains/TestInternalGrains/SerializerPresenceTestGrain.cs +++ b/test/Grains/TestInternalGrains/SerializerPresenceTestGrain.cs @@ -8,7 +8,7 @@ internal class SerializerPresenceTestGrain : Grain, ISerializerPresenceTest { public Task SerializerExistsForType(Type t) { - return Task.FromResult(this.ServiceProvider.GetRequiredService().CanSerialize(t)); + return Task.FromResult(ServiceProvider!.GetRequiredService().CanSerialize(t)); } public Task TakeSerializedData(object data) diff --git a/test/Grains/TestInternalGrains/StreamingGrain.cs b/test/Grains/TestInternalGrains/StreamingGrain.cs index 12eb03dd38b..99ab1526b13 100644 --- a/test/Grains/TestInternalGrains/StreamingGrain.cs +++ b/test/Grains/TestInternalGrains/StreamingGrain.cs @@ -467,7 +467,7 @@ public Streaming_ProducerGrain(IGrainContext grainContext) public override Task OnActivateAsync(CancellationToken cancellationToken) { var activationId = _grainContext.ActivationId; - _logger = this.ServiceProvider.GetRequiredService().CreateLogger("Test.Streaming_ProducerGrain " + RuntimeIdentity + "/" + IdentityString + "/" + activationId); + _logger = ServiceProvider!.GetRequiredService().CreateLogger("Test.Streaming_ProducerGrain " + RuntimeIdentity + "/" + IdentityString + "/" + activationId); _logger.LogInformation("OnActivateAsync"); _producers = new List(); _cleanedUpFlag = new InterlockedFlag(); @@ -584,7 +584,7 @@ public override async Task OnActivateAsync(CancellationToken cancellationToken) { await base.OnActivateAsync(cancellationToken); var activationId = _grainContext.ActivationId; - _logger = this.ServiceProvider.GetRequiredService().CreateLogger("Test.PersistentStreaming_ProducerGrain " + RuntimeIdentity + "/" + IdentityString + "/" + activationId); + _logger = ServiceProvider!.GetRequiredService().CreateLogger("Test.PersistentStreaming_ProducerGrain " + RuntimeIdentity + "/" + IdentityString + "/" + activationId); _logger.LogInformation("OnActivateAsync"); if (State.Producers == null) { @@ -658,7 +658,7 @@ public Streaming_ConsumerGrain(IGrainContext grainContext) public override Task OnActivateAsync(CancellationToken cancellationToken) { var activationId = _grainContext.ActivationId; - _logger = this.ServiceProvider.GetRequiredService().CreateLogger("Test.Streaming_ConsumerGrain " + RuntimeIdentity + "/" + IdentityString + "/" + activationId); + _logger = ServiceProvider!.GetRequiredService().CreateLogger("Test.Streaming_ConsumerGrain " + RuntimeIdentity + "/" + IdentityString + "/" + activationId); _logger.LogInformation("OnActivateAsync"); _observers = new List(); return Task.CompletedTask; @@ -722,7 +722,7 @@ public override async Task OnActivateAsync(CancellationToken cancellationToken) { await base.OnActivateAsync(cancellationToken); var activationId = _grainContext.ActivationId; - _logger = this.ServiceProvider.GetRequiredService().CreateLogger("Test.PersistentStreaming_ConsumerGrain " + RuntimeIdentity + "/" + IdentityString + "/" + activationId); + _logger = ServiceProvider!.GetRequiredService().CreateLogger("Test.PersistentStreaming_ConsumerGrain " + RuntimeIdentity + "/" + IdentityString + "/" + activationId); _logger.LogInformation("OnActivateAsync"); if (State.Consumers == null) @@ -770,7 +770,7 @@ public class Streaming_Reentrant_ProducerConsumerGrain : Streaming_ProducerConsu public override async Task OnActivateAsync(CancellationToken cancellationToken) { var activationId = RuntimeContext.Current!.ActivationId; - _logger = this.ServiceProvider.GetRequiredService().CreateLogger("Test.Streaming_Reentrant_ProducerConsumerGrain " + RuntimeIdentity + "/" + IdentityString + "/" + activationId) ; + _logger = ServiceProvider!.GetRequiredService().CreateLogger("Test.Streaming_Reentrant_ProducerConsumerGrain " + RuntimeIdentity + "/" + IdentityString + "/" + activationId) ; _logger.LogInformation("OnActivateAsync"); await base.OnActivateAsync(cancellationToken); } @@ -786,7 +786,7 @@ public class Streaming_ProducerConsumerGrain : Grain, IStreaming_ProducerConsume public override Task OnActivateAsync(CancellationToken cancellationToken) { var activationId = RuntimeContext.Current!.ActivationId; - _logger = this.ServiceProvider.GetRequiredService().CreateLogger("Test.Streaming_ProducerConsumerGrain " + RuntimeIdentity + "/" + IdentityString + "/" + activationId); + _logger = ServiceProvider!.GetRequiredService().CreateLogger("Test.Streaming_ProducerConsumerGrain " + RuntimeIdentity + "/" + IdentityString + "/" + activationId); _logger.LogInformation("OnActivateAsync"); return Task.CompletedTask; } @@ -897,7 +897,7 @@ public abstract class Streaming_ImplicitlySubscribedConsumerGrainBase : Grain, I public override Task OnActivateAsync(CancellationToken cancellationToken) { var activationId = RuntimeContext.Current!.ActivationId; - _logger = this.ServiceProvider.GetRequiredService().CreateLogger("Test.Streaming_ImplicitConsumerGrain1 " + RuntimeIdentity + "/" + IdentityString + "/" + activationId); + _logger = ServiceProvider!.GetRequiredService().CreateLogger("Test.Streaming_ImplicitConsumerGrain1 " + RuntimeIdentity + "/" + IdentityString + "/" + activationId); _logger.LogInformation("{Type}.OnActivateAsync", GetType().FullName); _observers = new Dictionary(); return Task.CompletedTask; diff --git a/test/Grains/TestInternalGrains/TestGrain.cs b/test/Grains/TestInternalGrains/TestGrain.cs index ba96cbb8579..64ef918f56c 100644 --- a/test/Grains/TestInternalGrains/TestGrain.cs +++ b/test/Grains/TestInternalGrains/TestGrain.cs @@ -204,7 +204,7 @@ public Task GetActivationId() return Task.FromResult(_id); } - public Task GetSiloAddress() => Task.FromResult(ServiceProvider.GetRequiredService().SiloAddress); + public Task GetSiloAddress() => Task.FromResult(ServiceProvider!.GetRequiredService().SiloAddress); } internal class OneWayGrain : Grain, IOneWayGrain, ISimpleGrainObserver @@ -219,8 +219,8 @@ internal class OneWayGrain : Grain, IOneWayGrain, ISimpleGrainObserver public OneWayGrain(GrainLocator grainLocator) => this.grainLocator = grainLocator; - private ILocalGrainDirectory LocalGrainDirectory => this.ServiceProvider.GetRequiredService(); - private ILocalSiloDetails LocalSiloDetails => this.ServiceProvider.GetRequiredService(); + private ILocalGrainDirectory LocalGrainDirectory => ServiceProvider!.GetRequiredService(); + private ILocalSiloDetails LocalSiloDetails => ServiceProvider!.GetRequiredService(); public Task Notify() { @@ -264,7 +264,7 @@ public async Task GetOtherGrain() async Task GetGrainOnOtherSilo() { - var silos = ServiceProvider.GetRequiredService().CurrentSnapshot.Members.Where(kv => kv.Value.Status == SiloStatus.Active).Select(kv => kv.Key).ToHashSet(); + var silos = ServiceProvider!.GetRequiredService().CurrentSnapshot.Members.Where(kv => kv.Value.Status == SiloStatus.Active).Select(kv => kv.Key).ToHashSet(); var thisSilo = await this.GetSiloAddress(); silos.Remove(thisSilo); while (true) diff --git a/test/Orleans.Core.Tests/Runtime/GrainRuntimeResolutionTests.cs b/test/Orleans.Core.Tests/Runtime/GrainRuntimeResolutionTests.cs new file mode 100644 index 00000000000..d452e3563af --- /dev/null +++ b/test/Orleans.Core.Tests/Runtime/GrainRuntimeResolutionTests.cs @@ -0,0 +1,358 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using Microsoft.Extensions.DependencyInjection; +using NSubstitute; +using Orleans; +using Orleans.Runtime; +using Orleans.Runtime.Placement; +using TestExtensions; +using Xunit; + +namespace UnitTests.Runtime; + +[TestSuite("BVT")] +[TestProvider("None")] +[TestArea("Runtime")] +public class GrainRuntimeResolutionTests +{ + [Fact, TestCategory("BVT")] + public void Constructor_DoesNotResolveRuntime() + { + var grainContext = new TestGrainContext(); + + _ = new TestGrain(grainContext); + + Assert.Equal(0, grainContext.GetComponentCallCount); + Assert.Equal(0, grainContext.ActivationServicesAccessCount); + } + + [Fact, TestCategory("BVT")] + public void Runtime_ContextComponent_ReturnsRuntime() + { + var grainRuntime = Substitute.For(); + var grainContext = new TestGrainContext(); + grainContext.SetComponent(grainRuntime); + var grain = new TestGrain(grainContext); + + var first = grain.Runtime; + var second = grain.Runtime; + + Assert.Same(grainRuntime, first); + Assert.Same(first, second); + Assert.Equal(2, grainContext.GetComponentCallCount); + Assert.Equal(0, grainContext.ActivationServicesAccessCount); + } + + [Fact, TestCategory("BVT")] + public async Task Runtime_ConcurrentAccess_UsesContextRuntime() + { + var grainRuntime = Substitute.For(); + var grainContext = new TestGrainContext(); + grainContext.SetComponent(grainRuntime); + var grain = new TestGrain(grainContext); + + var results = await Task.WhenAll(Enumerable.Range(0, 32).Select(_ => Task.Run(() => grain.Runtime))); + + Assert.All(results, result => Assert.Same(grainRuntime, result)); + Assert.Equal(results.Length, grainContext.GetComponentCallCount); + } + + [Fact, TestCategory("BVT")] + public void Runtime_ExplicitRuntime_RegistersContextComponent() + { + var grainContext = new TestGrainContext(); + var grainRuntime = Substitute.For(); + var grain = new TestGrain(grainContext, grainRuntime); + + Assert.Same(grainRuntime, ((IGrainContext)grainContext).GrainRuntime); + Assert.Same(grainRuntime, grain.Runtime); + Assert.Equal(1, grainContext.SetComponentCallCount); + Assert.Equal(0, grainContext.ActivationServicesAccessCount); + } + + [Fact, TestCategory("BVT")] + public void Runtime_ExplicitRuntime_WorksWithMockContext() + { + var grainContext = Substitute.For(); + var grainRuntime = Substitute.For(); + var grain = new TestGrain(grainContext, grainRuntime); + + Assert.Same(grainRuntime, grainContext.GrainRuntime); + Assert.Same(grainRuntime, grain.Runtime); + } + + [Fact, TestCategory("BVT")] + public void Runtime_ContextComponent_TakesPrecedenceOverActivationServices() + { + var componentRuntime = Substitute.For(); + var serviceRuntime = Substitute.For(); + using var serviceProvider = new ServiceCollection() + .AddSingleton(serviceRuntime) + .BuildServiceProvider(); + var grainContext = new TestGrainContext(serviceProvider); + grainContext.SetComponent(componentRuntime); + var grain = new TestGrain(grainContext); + + Assert.Same(componentRuntime, grain.Runtime); + Assert.Equal(0, grainContext.ActivationServicesAccessCount); + } + + [Fact, TestCategory("BVT")] + public void Runtime_ContextComponentUnavailable_UsesActivationServices() + { + var grainRuntime = Substitute.For(); + using var serviceProvider = new ServiceCollection() + .AddSingleton(grainRuntime) + .BuildServiceProvider(); + var grainContext = new TestGrainContext(serviceProvider); + var grain = new TestGrain(grainContext); + + Assert.Same(grainRuntime, grain.Runtime); + Assert.Equal(1, grainContext.GetComponentCallCount); + Assert.Equal(1, grainContext.ActivationServicesAccessCount); + } + + [Fact, TestCategory("BVT")] + public void Runtime_Unavailable_ThrowsDeterministicException() + { + using var serviceProvider = new ServiceCollection().BuildServiceProvider(); + var grain = new TestGrain(new TestGrainContext(serviceProvider)); + + var exception = Assert.Throws(() => grain.Runtime); + + Assert.Equal("Grain was created outside of the Orleans creation process and no runtime was specified.", exception.Message); + } + + [Fact, TestCategory("BVT")] + public void Runtime_NullActivationServices_ThrowsDeterministicException() + { + var grain = new TestGrain(new TestGrainContext()); + + var exception = Assert.Throws(() => grain.Runtime); + + Assert.Equal("Grain was created outside of the Orleans creation process and no runtime was specified.", exception.Message); + } + + [Fact, TestCategory("BVT")] + public void DirectConstruction_WithoutRuntime_PreservesOptionalRuntimeBehavior() + { + var grain = new TestGrain(null!); + + Assert.Null(grain.ServiceProvider); + Assert.Empty(grain.RuntimeIdentity); + } + + [Fact, TestCategory("BVT")] + public void DirectConstruction_WithExplicitRuntime_PreservesRuntimeBehavior() + { + using var serviceProvider = new ServiceCollection().BuildServiceProvider(); + var grainRuntime = Substitute.For(); + grainRuntime.ServiceProvider.Returns(serviceProvider); + grainRuntime.SiloIdentity.Returns("test-silo"); + + var grain = new TestGrain(null!, grainRuntime); + + Assert.Same(grainRuntime, grain.Runtime); + Assert.Same(serviceProvider, grain.ServiceProvider); + Assert.Equal("test-silo", grain.RuntimeIdentity); + } + + [Fact, TestCategory("BVT")] + public async Task Runtime_RemainsAvailableAcrossLifecycleCallbacks() + { + var grainRuntime = Substitute.For(); + var grainContext = new TestGrainContext(); + grainContext.SetComponent(grainRuntime); + var grain = new LifecycleGrain(grainContext); + + await grain.OnActivateAsync(CancellationToken.None); + await grain.OnDeactivateAsync(new(DeactivationReasonCode.ApplicationRequested, "test"), CancellationToken.None); + + Assert.Same(grainRuntime, grain.ActivationRuntime); + Assert.Same(grainRuntime, grain.DeactivationRuntime); + } + + [Fact, TestCategory("BVT")] + public void Grain_DoesNotRetainRuntimeField() + { + var runtimeFields = typeof(Grain) + .GetFields(BindingFlags.Instance | BindingFlags.NonPublic) + .Where(field => field.FieldType == typeof(IGrainRuntime)); + + Assert.Empty(runtimeFields); + } + + [Fact, TestCategory("BVT")] + public void ActivationContext_RuntimeOverride_IsPerActivation() + { + var defaultRuntime = Substitute.For(); + var overrideRuntime = Substitute.For(); + var shared = CreateSharedContext(defaultRuntime); + var first = CreateActivationData(shared); + var second = CreateActivationData(shared); + + ((IGrainContext)first).GrainRuntime = overrideRuntime; + + Assert.Same(overrideRuntime, first.GrainRuntime); + Assert.Same(defaultRuntime, second.GrainRuntime); + Assert.Same(defaultRuntime, shared.Runtime); + } + + [Fact, TestCategory("BVT")] + public async Task ActivationContext_RuntimeOverride_IsSafelyPublished() + { + var defaultRuntime = Substitute.For(); + var overrideRuntime = Substitute.For(); + var activation = CreateActivationData(CreateSharedContext(defaultRuntime)); + var context = (IGrainContext)activation; + + var writer = Task.Run(() => + { + for (var i = 0; i < 100_000; i++) + { + context.GrainRuntime = (i & 1) == 0 ? overrideRuntime : null; + } + }, TestContext.Current.CancellationToken); + var reader = Task.Run(() => + { + for (var i = 0; i < 100_000; i++) + { + var runtime = context.GrainRuntime; + Assert.True(ReferenceEquals(runtime, defaultRuntime) || ReferenceEquals(runtime, overrideRuntime)); + } + }, TestContext.Current.CancellationToken); + + await Task.WhenAll(writer, reader); + } + + [Fact, TestCategory("BVT")] + public void ActivationContext_ClearingAbsentRuntimeOverride_DoesNotCreateExtras() + { + var activation = CreateActivationData(CreateSharedContext(Substitute.For())); + + ((IGrainContext)activation).GrainRuntime = null; + + Assert.Null(typeof(ActivationData) + .GetField("_extras", BindingFlags.Instance | BindingFlags.NonPublic)! + .GetValue(activation)); + } + + [Fact, TestCategory("BVT")] + public void StatelessWorkerContext_RuntimeOverride_DoesNotMutateSharedContext() + { + var defaultRuntime = Substitute.For(); + var overrideRuntime = Substitute.For(); + var shared = CreateSharedContext(defaultRuntime); + var statelessShared = (StatelessWorkerGrainTypeSharedContext)RuntimeHelpers.GetUninitializedObject(typeof(StatelessWorkerGrainTypeSharedContext)); + typeof(StatelessWorkerGrainTypeSharedContext) + .GetField("k__BackingField", BindingFlags.Instance | BindingFlags.NonPublic)! + .SetValue(statelessShared, shared); + var context = (StatelessWorkerGrainContext)RuntimeHelpers.GetUninitializedObject(typeof(StatelessWorkerGrainContext)); + typeof(StatelessWorkerGrainContext) + .GetField("_shared", BindingFlags.Instance | BindingFlags.NonPublic)! + .SetValue(context, statelessShared); + + ((IGrainContext)context).GrainRuntime = defaultRuntime; + var exception = Assert.Throws(() => ((IGrainContext)context).GrainRuntime = overrideRuntime); + + Assert.Equal("value", exception.ParamName); + Assert.Same(defaultRuntime, context.GrainRuntime); + Assert.Same(defaultRuntime, shared.Runtime); + } + + private static GrainTypeSharedContext CreateSharedContext(IGrainRuntime runtime) + { + var shared = (GrainTypeSharedContext)RuntimeHelpers.GetUninitializedObject(typeof(GrainTypeSharedContext)); + typeof(GrainTypeSharedContext) + .GetField("k__BackingField", BindingFlags.Instance | BindingFlags.NonPublic)! + .SetValue(shared, runtime); + return shared; + } + + private static ActivationData CreateActivationData(GrainTypeSharedContext shared) + { + var activation = (ActivationData)RuntimeHelpers.GetUninitializedObject(typeof(ActivationData)); + typeof(ActivationData) + .GetField("_shared", BindingFlags.Instance | BindingFlags.NonPublic)! + .SetValue(activation, shared); + return activation; + } + + private sealed class TestGrain(IGrainContext grainContext, IGrainRuntime? grainRuntime = null) + : Grain(grainContext, grainRuntime); + + private sealed class LifecycleGrain(IGrainContext grainContext) : Grain(grainContext) + { + public IGrainRuntime? ActivationRuntime { get; private set; } + + public IGrainRuntime? DeactivationRuntime { get; private set; } + + public override Task OnActivateAsync(CancellationToken cancellationToken) + { + ActivationRuntime = Runtime; + return Task.CompletedTask; + } + + public override Task OnDeactivateAsync(DeactivationReason reason, CancellationToken cancellationToken) + { + DeactivationRuntime = Runtime; + return Task.CompletedTask; + } + } + + private sealed class TestGrainContext(IServiceProvider? activationServices = null) : IGrainContext + { + private readonly Dictionary _components = []; + + public int ActivationServicesAccessCount; + public int GetComponentCallCount; + public int SetComponentCallCount; + + public GrainReference GrainReference => throw new NotImplementedException(); + public GrainId GrainId => default; + public object? GrainInstance => null; + public ActivationId ActivationId => default; + public GrainAddress Address => throw new NotImplementedException(); + public IServiceProvider ActivationServices + { + get + { + Interlocked.Increment(ref ActivationServicesAccessCount); + return activationServices!; + } + } + + public IGrainLifecycle ObservableLifecycle => throw new NotImplementedException(); + public IWorkItemScheduler Scheduler => throw new NotImplementedException(); + public Task Deactivated => Task.CompletedTask; + public PlacementStrategy PlacementStrategy => throw new NotImplementedException(); + + public object? GetComponent(Type componentType) + { + Interlocked.Increment(ref GetComponentCallCount); + return _components.TryGetValue(componentType, out var component) ? component : null; + } + + public object? GetTarget() => null; + + public void SetComponent(TComponent? value) where TComponent : class + { + Interlocked.Increment(ref SetComponentCallCount); + if (value is null) + { + _components.Remove(typeof(TComponent)); + } + else + { + _components[typeof(TComponent)] = value; + } + } + + public void ReceiveMessage(object message) => throw new NotImplementedException(); + public void Activate(Dictionary? requestContext, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + public void Deactivate(DeactivationReason deactivationReason, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + public void Rehydrate(IRehydrationContext context) => throw new NotImplementedException(); + public void Migrate(Dictionary? requestContext, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + public bool Equals(IGrainContext? other) => ReferenceEquals(this, other); + } +} diff --git a/test/Orleans.Placement.Tests/ActivationRepartitioningTests/CustomToleranceTests.cs b/test/Orleans.Placement.Tests/ActivationRepartitioningTests/CustomToleranceTests.cs index 3094cd53d82..ad6b511a7b6 100644 --- a/test/Orleans.Placement.Tests/ActivationRepartitioningTests/CustomToleranceTests.cs +++ b/test/Orleans.Placement.Tests/ActivationRepartitioningTests/CustomToleranceTests.cs @@ -185,13 +185,13 @@ public async Task FirstPing(SiloAddress silo2) public Task GetAddress() => Task.FromResult(GrainContext.Address.SiloAddress!); public override Task OnActivateAsync(CancellationToken cancellationToken) { - ServiceProvider.GetRequiredService>().LogInformation("Activating {GrainId} on silo {SiloAddress}", this.GrainId, this.Runtime.SiloAddress); + ServiceProvider!.GetRequiredService>().LogInformation("Activating {GrainId} on silo {SiloAddress}", this.GrainId, this.Runtime.SiloAddress); return base.OnActivateAsync(cancellationToken); } public override Task OnDeactivateAsync(DeactivationReason reason, CancellationToken cancellationToken) { - ServiceProvider.GetRequiredService>().LogInformation("Deactivating {GrainId} on silo {SiloAddress}. Reason: {Reason}", this.GrainId, this.Runtime.SiloAddress, reason); + ServiceProvider!.GetRequiredService>().LogInformation("Deactivating {GrainId} on silo {SiloAddress}. Reason: {Reason}", this.GrainId, this.Runtime.SiloAddress, reason); return base.OnDeactivateAsync(reason, cancellationToken); } } @@ -204,13 +204,13 @@ public class F : Grain, IF public override Task OnActivateAsync(CancellationToken cancellationToken) { - ServiceProvider.GetRequiredService>().LogInformation("Activating {GrainId} on silo {SiloAddress}", this.GrainId, this.Runtime.SiloAddress); + ServiceProvider!.GetRequiredService>().LogInformation("Activating {GrainId} on silo {SiloAddress}", this.GrainId, this.Runtime.SiloAddress); return base.OnActivateAsync(cancellationToken); } public override Task OnDeactivateAsync(DeactivationReason reason, CancellationToken cancellationToken) { - ServiceProvider.GetRequiredService>().LogInformation("Deactivating {GrainId} on silo {SiloAddress}. Reason: {Reason}", this.GrainId, this.Runtime.SiloAddress, reason); + ServiceProvider!.GetRequiredService>().LogInformation("Deactivating {GrainId} on silo {SiloAddress}. Reason: {Reason}", this.GrainId, this.Runtime.SiloAddress, reason); return base.OnDeactivateAsync(reason, cancellationToken); } } @@ -227,13 +227,13 @@ public class X : Grain, IX public override Task OnActivateAsync(CancellationToken cancellationToken) { - ServiceProvider.GetRequiredService>().LogInformation("Activating {GrainId} on silo {SiloAddress}", this.GrainId, this.Runtime.SiloAddress); + ServiceProvider!.GetRequiredService>().LogInformation("Activating {GrainId} on silo {SiloAddress}", this.GrainId, this.Runtime.SiloAddress); return base.OnActivateAsync(cancellationToken); } public override Task OnDeactivateAsync(DeactivationReason reason, CancellationToken cancellationToken) { - ServiceProvider.GetRequiredService>().LogInformation("Deactivating {GrainId} on silo {SiloAddress}. Reason: {Reason}", this.GrainId, this.Runtime.SiloAddress, reason); + ServiceProvider!.GetRequiredService>().LogInformation("Deactivating {GrainId} on silo {SiloAddress}. Reason: {Reason}", this.GrainId, this.Runtime.SiloAddress, reason); return base.OnDeactivateAsync(reason, cancellationToken); } } diff --git a/test/TestInfrastructure/Orleans.TestingHost.Tests/Grains/BlockingCallGrains.cs b/test/TestInfrastructure/Orleans.TestingHost.Tests/Grains/BlockingCallGrains.cs index bdb5ac6feef..6627bf46ce1 100644 --- a/test/TestInfrastructure/Orleans.TestingHost.Tests/Grains/BlockingCallGrains.cs +++ b/test/TestInfrastructure/Orleans.TestingHost.Tests/Grains/BlockingCallGrains.cs @@ -48,7 +48,7 @@ public static Task WaitForEntered(Guid key, TimeSpan timeout, Cancellation => GetState(key).Entered.WaitAsync(timeout, cancellationToken); public Task GetSiloIdentity() => - Task.FromResult(this.ServiceProvider.GetRequiredService().SiloAddress.ToString()); + Task.FromResult(ServiceProvider!.GetRequiredService().SiloAddress.ToString()); public Task BlockUntilReleased() { @@ -87,7 +87,7 @@ public async Task CallRemote(IRemoteBlockerGrain remote) public class LauncherGrain : Grain, ILauncherGrain { public Task GetSiloIdentity() => - Task.FromResult(this.ServiceProvider.GetRequiredService().SiloAddress.ToString()); + Task.FromResult(ServiceProvider!.GetRequiredService().SiloAddress.ToString()); public Task StartBlockingCall(ILocalWorkerGrain worker, IRemoteBlockerGrain remote) {