From 33dfbd26efd2448eb5816a2d95d3e604dbb22280 Mon Sep 17 00:00:00 2001 From: Reuben Bond Date: Wed, 12 Aug 2026 10:30:52 -0700 Subject: [PATCH 1/3] fix(runtime): configure grain context before construction Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../ActivationDataActivatorProvider.cs | 11 ++- .../Activation/IGrainContextActivator.cs | 10 +++ .../Runtime/GrainContextActivatorTests.cs | 70 ++++++++++++++++ .../GrainActivatorTests.cs | 79 ++++++++++++++++++- 4 files changed, 167 insertions(+), 3 deletions(-) create mode 100644 test/Orleans.Core.Tests/Runtime/GrainContextActivatorTests.cs diff --git a/src/Orleans.Runtime/Activation/ActivationDataActivatorProvider.cs b/src/Orleans.Runtime/Activation/ActivationDataActivatorProvider.cs index ab09a5d99d0..348c8dcf830 100644 --- a/src/Orleans.Runtime/Activation/ActivationDataActivatorProvider.cs +++ b/src/Orleans.Runtime/Activation/ActivationDataActivatorProvider.cs @@ -49,7 +49,7 @@ public bool TryGet(GrainType grainType, [NotNullWhen(true)] out IGrainContextAct return true; } - private partial class ActivationDataActivator : IGrainContextActivator + private partial class ActivationDataActivator : IGrainContextActivatorWithConfiguration { private readonly IOptions _schedulingOptions; private readonly IGrainActivator _grainActivator; @@ -76,7 +76,9 @@ public ActivationDataActivator( _startActivation = state => ((ActivationData)state!).Start(_grainActivator); } - public IGrainContext CreateContext(GrainAddress activationAddress) + public IGrainContext CreateContext(GrainAddress activationAddress) => CreateContext(activationAddress, []); + + public IGrainContext CreateContext(GrainAddress activationAddress, IConfigureGrainContext[] configureActions) { var context = new ActivationData( activationAddress, @@ -84,6 +86,11 @@ public IGrainContext CreateContext(GrainAddress activationAddress) _serviceProvider, _sharedComponents); + foreach (var configure in configureActions) + { + configure.Configure(context); + } + using var ecSuppressor = ExecutionContext.SuppressFlow(); _ = Task.Factory.StartNew( _startActivation, diff --git a/src/Orleans.Runtime/Activation/IGrainContextActivator.cs b/src/Orleans.Runtime/Activation/IGrainContextActivator.cs index e7dd84ba8a3..aeef900dec9 100644 --- a/src/Orleans.Runtime/Activation/IGrainContextActivator.cs +++ b/src/Orleans.Runtime/Activation/IGrainContextActivator.cs @@ -64,6 +64,11 @@ public IGrainContext CreateInstance(GrainAddress address) activator = this.CreateActivator(grainId.Type); } + if (activator.Activator is IGrainContextActivatorWithConfiguration configurableActivator) + { + return configurableActivator.CreateContext(address, activator.ConfigureActions); + } + var result = activator.Activator.CreateContext(address); foreach (var configure in activator.ConfigureActions) { @@ -139,6 +144,11 @@ public interface IGrainContextActivator public IGrainContext CreateContext(GrainAddress address); } + internal interface IGrainContextActivatorWithConfiguration : IGrainContextActivator + { + IGrainContext CreateContext(GrainAddress address, IConfigureGrainContext[] configureActions); + } + /// /// Provides a instance for the provided grain type. /// diff --git a/test/Orleans.Core.Tests/Runtime/GrainContextActivatorTests.cs b/test/Orleans.Core.Tests/Runtime/GrainContextActivatorTests.cs new file mode 100644 index 00000000000..ca4866bee32 --- /dev/null +++ b/test/Orleans.Core.Tests/Runtime/GrainContextActivatorTests.cs @@ -0,0 +1,70 @@ +using System.Collections.Generic; +using NSubstitute; +using Orleans.Metadata; +using Orleans.Runtime; +using TestExtensions; +using Xunit; + +namespace UnitTests.Runtime; + +public class GrainContextActivatorTests +{ + [Fact, TestCategory("BVT")] + public void CreateInstance_ConfiguresContextBeforeStartingActivation() + { + var events = new List(); + var context = Substitute.For(); + var contextActivator = new TestGrainContextActivator(context, events); + var activator = new GrainContextActivator( + [new TestGrainContextActivatorProvider(contextActivator)], + [new TestConfigureGrainContextProvider(events)], + new GrainPropertiesResolver(Substitute.For())); + var address = new GrainAddress { GrainId = GrainId.Create("test", "grain") }; + + Assert.Same(context, activator.CreateInstance(address)); + Assert.Equal(["configure", "activate"], events); + } + + private sealed class TestGrainContextActivatorProvider(IGrainContextActivator activator) : IGrainContextActivatorProvider + { + public bool TryGet(GrainType grainType, out IGrainContextActivator result) + { + result = activator; + return true; + } + } + + private sealed class TestConfigureGrainContextProvider(List events) : IConfigureGrainContextProvider + { + public bool TryGetConfigurator(GrainType grainType, GrainProperties properties, out IConfigureGrainContext configurator) + { + configurator = new TestConfigureGrainContext(events); + return true; + } + } + + private sealed class TestConfigureGrainContext(List events) : IConfigureGrainContext + { + public void Configure(IGrainContext context) => events.Add("configure"); + } + + private sealed class TestGrainContextActivator(IGrainContext context, List events) : IGrainContextActivatorWithConfiguration + { + public IGrainContext CreateContext(GrainAddress address) + { + events.Add("activate"); + return context; + } + + public IGrainContext CreateContext(GrainAddress address, IConfigureGrainContext[] configureActions) + { + foreach (var configure in configureActions) + { + configure.Configure(context); + } + + events.Add("activate"); + return context; + } + } +} diff --git a/test/Orleans.Runtime.Tests/GrainActivatorTests.cs b/test/Orleans.Runtime.Tests/GrainActivatorTests.cs index 2bf31339b4d..cf9d3407190 100644 --- a/test/Orleans.Runtime.Tests/GrainActivatorTests.cs +++ b/test/Orleans.Runtime.Tests/GrainActivatorTests.cs @@ -1,12 +1,13 @@ +using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging.Abstractions; +using Orleans.Metadata; using Orleans.Runtime; using Orleans.TestingHost; using TestExtensions; using UnitTests.GrainInterfaces; using UnitTests.Grains; using Xunit; -using Orleans.Metadata; namespace UnitTests.General { @@ -48,6 +49,7 @@ public void Configure(ISiloBuilder hostBuilder) // Register our custom grain activator as a grain type component configurator // This allows it to selectively apply to specific grain types services.AddSingleton(); + services.AddSingleton(); }); } } @@ -96,6 +98,20 @@ public async Task CanUseCustomGrainActivatorToReleaseGrains() Assert.Equal(initialReleasedInstances + 1, finalReleasedInstances); } + [Fact, TestCategory("BVT")] + public async Task GrainContextIsConfiguredBeforeGrainConstruction() + { + var state = ActivationOrderingState.Instance; + state.Arm(); + var grain = this.fixture.GrainFactory.GetGrain( + GetRandomGrainId(), + grainClassNamePrefix: "UnitTests.Grains.ExplicitlyRegistered"); + + await grain.GetStringValue(); + + Assert.True(state.WasConfiguredAtConstruction); + } + /// /// Custom grain activator that bypasses dependency injection entirely. /// Implements both IGrainActivator (for creation/disposal) and IConfigureGrainTypeComponents @@ -127,6 +143,8 @@ public void Configure(GrainType grainType, GrainProperties properties, GrainType public object CreateInstance(IGrainContext context) { + ActivationOrderingState.Instance.ObserveConstruction(context); + // Custom instantiation logic - creates grain with hardcoded dependencies // In real scenarios, this could get objects from a pool, perform complex // initialization, or integrate with external systems @@ -141,5 +159,64 @@ public ValueTask DisposeInstance(IGrainContext context, object instance) return default; } } + + private sealed class ActivationOrderingConfiguratorProvider(GrainClassMap grainClassMap) : IConfigureGrainContextProvider + { + public bool TryGetConfigurator( + GrainType grainType, + GrainProperties properties, + [NotNullWhen(true)] out IConfigureGrainContext? configurator) + { + if (grainClassMap.TryGetGrainClass(grainType, out var grainClass) + && grainClass.IsAssignableFrom(typeof(ExplicitlyRegisteredSimpleDIGrain))) + { + configurator = ActivationOrderingState.Instance; + return true; + } + + configurator = null; + return false; + } + } + + private sealed class ActivationOrderingState : IConfigureGrainContext + { + private int _armed; + + public static ActivationOrderingState Instance { get; } = new(); + + public bool WasConfiguredAtConstruction { get; private set; } + + public void Arm() + { + WasConfiguredAtConstruction = false; + Volatile.Write(ref _armed, 1); + } + + public void Configure(IGrainContext context) + { + if (Volatile.Read(ref _armed) == 0) + { + return; + } + + context.SetComponent(ConfiguredContextMarker.Instance); + } + + public void ObserveConstruction(IGrainContext context) + { + if (Interlocked.Exchange(ref _armed, 0) == 0) + { + return; + } + + WasConfiguredAtConstruction = context.GetComponent() is not null; + } + } + + private sealed class ConfiguredContextMarker + { + public static ConfiguredContextMarker Instance { get; } = new(); + } } } From 1b01817e0d83287162f85554760527637ea9978c Mon Sep 17 00:00:00 2001 From: Reuben Bond Date: Wed, 12 Aug 2026 13:00:21 -0700 Subject: [PATCH 2/3] feat(runtime)!: require context configuration during creation BREAKING CHANGE: IGrainContextActivator.CreateContext now receives the context configuration actions and must apply them before grain construction begins. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 408b1948-f836-4328-9b59-c1a6d9504551 --- .../ActivationDataActivatorProvider.cs | 15 +++++++++---- .../Activation/IGrainContextActivator.cs | 21 +++---------------- .../Catalog/StatelessWorkerGrainContext.cs | 2 +- src/api/Orleans.Runtime/Orleans.Runtime.cs | 2 +- .../Runtime/GrainContextActivatorTests.cs | 8 +------ 5 files changed, 17 insertions(+), 31 deletions(-) diff --git a/src/Orleans.Runtime/Activation/ActivationDataActivatorProvider.cs b/src/Orleans.Runtime/Activation/ActivationDataActivatorProvider.cs index 348c8dcf830..fb0d1cb48f7 100644 --- a/src/Orleans.Runtime/Activation/ActivationDataActivatorProvider.cs +++ b/src/Orleans.Runtime/Activation/ActivationDataActivatorProvider.cs @@ -49,7 +49,7 @@ public bool TryGet(GrainType grainType, [NotNullWhen(true)] out IGrainContextAct return true; } - private partial class ActivationDataActivator : IGrainContextActivatorWithConfiguration + private partial class ActivationDataActivator : IGrainContextActivator { private readonly IOptions _schedulingOptions; private readonly IGrainActivator _grainActivator; @@ -76,8 +76,6 @@ public ActivationDataActivator( _startActivation = state => ((ActivationData)state!).Start(_grainActivator); } - public IGrainContext CreateContext(GrainAddress activationAddress) => CreateContext(activationAddress, []); - public IGrainContext CreateContext(GrainAddress activationAddress, IConfigureGrainContext[] configureActions) { var context = new ActivationData( @@ -105,5 +103,14 @@ public IGrainContext CreateContext(GrainAddress activationAddress, IConfigureGra internal class StatelessWorkerActivator(StatelessWorkerGrainTypeSharedContext sharedContext, IGrainContextActivator innerActivator) : IGrainContextActivator { - public IGrainContext CreateContext(GrainAddress address) => new StatelessWorkerGrainContext(address, sharedContext, innerActivator); + public IGrainContext CreateContext(GrainAddress address, IConfigureGrainContext[] configureActions) + { + var result = new StatelessWorkerGrainContext(address, sharedContext, innerActivator); + foreach (var configure in configureActions) + { + configure.Configure(result); + } + + return result; + } } diff --git a/src/Orleans.Runtime/Activation/IGrainContextActivator.cs b/src/Orleans.Runtime/Activation/IGrainContextActivator.cs index aeef900dec9..ab496fbccb9 100644 --- a/src/Orleans.Runtime/Activation/IGrainContextActivator.cs +++ b/src/Orleans.Runtime/Activation/IGrainContextActivator.cs @@ -64,18 +64,7 @@ public IGrainContext CreateInstance(GrainAddress address) activator = this.CreateActivator(grainId.Type); } - if (activator.Activator is IGrainContextActivatorWithConfiguration configurableActivator) - { - return configurableActivator.CreateContext(address, activator.ConfigureActions); - } - - var result = activator.Activator.CreateContext(address); - foreach (var configure in activator.ConfigureActions) - { - configure.Configure(result); - } - - return result; + return activator.Activator.CreateContext(address, activator.ConfigureActions); } private (IGrainContextActivator, IConfigureGrainContext[]) CreateActivator(GrainType grainType) @@ -140,13 +129,9 @@ public interface IGrainContextActivator /// Creates a grain context for the given grain address. /// /// The grain address. + /// The actions which must be used to configure the context before grain construction begins. /// The newly created grain context. - public IGrainContext CreateContext(GrainAddress address); - } - - internal interface IGrainContextActivatorWithConfiguration : IGrainContextActivator - { - IGrainContext CreateContext(GrainAddress address, IConfigureGrainContext[] configureActions); + public IGrainContext CreateContext(GrainAddress address, IConfigureGrainContext[] configureActions); } /// diff --git a/src/Orleans.Runtime/Catalog/StatelessWorkerGrainContext.cs b/src/Orleans.Runtime/Catalog/StatelessWorkerGrainContext.cs index a51de49ae0d..327e6a368d3 100644 --- a/src/Orleans.Runtime/Catalog/StatelessWorkerGrainContext.cs +++ b/src/Orleans.Runtime/Catalog/StatelessWorkerGrainContext.cs @@ -320,7 +320,7 @@ private ActivationData CreateWorker(object? message) { Debug.Assert(!_terminated, "CreateWorker must not be called on a terminated stateless worker context."); var address = GrainAddress.GetAddress(Address.SiloAddress, Address.GrainId, ActivationId.NewId()); - var newWorker = (ActivationData)_innerActivator.CreateContext(address); + var newWorker = (ActivationData)_innerActivator.CreateContext(address, []); // Observe the create/destroy lifecycle of the activation newWorker.SetComponent(this); diff --git a/src/api/Orleans.Runtime/Orleans.Runtime.cs b/src/api/Orleans.Runtime/Orleans.Runtime.cs index 730989de04e..ece190a18f2 100644 --- a/src/api/Orleans.Runtime/Orleans.Runtime.cs +++ b/src/api/Orleans.Runtime/Orleans.Runtime.cs @@ -749,7 +749,7 @@ public partial interface IGrainActivator public partial interface IGrainContextActivator { - IGrainContext CreateContext(GrainAddress address); + IGrainContext CreateContext(GrainAddress address, IConfigureGrainContext[] configureActions); } public partial interface IGrainContextActivatorProvider diff --git a/test/Orleans.Core.Tests/Runtime/GrainContextActivatorTests.cs b/test/Orleans.Core.Tests/Runtime/GrainContextActivatorTests.cs index ca4866bee32..bf9ef3d755e 100644 --- a/test/Orleans.Core.Tests/Runtime/GrainContextActivatorTests.cs +++ b/test/Orleans.Core.Tests/Runtime/GrainContextActivatorTests.cs @@ -48,14 +48,8 @@ private sealed class TestConfigureGrainContext(List events) : IConfigure public void Configure(IGrainContext context) => events.Add("configure"); } - private sealed class TestGrainContextActivator(IGrainContext context, List events) : IGrainContextActivatorWithConfiguration + private sealed class TestGrainContextActivator(IGrainContext context, List events) : IGrainContextActivator { - public IGrainContext CreateContext(GrainAddress address) - { - events.Add("activate"); - return context; - } - public IGrainContext CreateContext(GrainAddress address, IConfigureGrainContext[] configureActions) { foreach (var configure in configureActions) From d3766893456017aaea0a1f59a2cfd68760372267 Mon Sep 17 00:00:00 2001 From: Reuben Bond Date: Wed, 12 Aug 2026 13:10:06 -0700 Subject: [PATCH 3/3] fix(runtime): configure stateless workers before startup Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 408b1948-f836-4328-9b59-c1a6d9504551 --- .../Activation/ActivationDataActivatorProvider.cs | 10 +--------- .../Catalog/StatelessWorkerGrainContext.cs | 8 +++++++- .../Runtime/GrainContextActivatorTests.cs | 8 ++++++-- test/Orleans.Runtime.Tests/GrainActivatorTests.cs | 13 ++++++++----- 4 files changed, 22 insertions(+), 17 deletions(-) diff --git a/src/Orleans.Runtime/Activation/ActivationDataActivatorProvider.cs b/src/Orleans.Runtime/Activation/ActivationDataActivatorProvider.cs index fb0d1cb48f7..4d14d65ce1c 100644 --- a/src/Orleans.Runtime/Activation/ActivationDataActivatorProvider.cs +++ b/src/Orleans.Runtime/Activation/ActivationDataActivatorProvider.cs @@ -104,13 +104,5 @@ public IGrainContext CreateContext(GrainAddress activationAddress, IConfigureGra internal class StatelessWorkerActivator(StatelessWorkerGrainTypeSharedContext sharedContext, IGrainContextActivator innerActivator) : IGrainContextActivator { public IGrainContext CreateContext(GrainAddress address, IConfigureGrainContext[] configureActions) - { - var result = new StatelessWorkerGrainContext(address, sharedContext, innerActivator); - foreach (var configure in configureActions) - { - configure.Configure(result); - } - - return result; - } + => new StatelessWorkerGrainContext(address, sharedContext, innerActivator, configureActions); } diff --git a/src/Orleans.Runtime/Catalog/StatelessWorkerGrainContext.cs b/src/Orleans.Runtime/Catalog/StatelessWorkerGrainContext.cs index 327e6a368d3..b244328caf0 100644 --- a/src/Orleans.Runtime/Catalog/StatelessWorkerGrainContext.cs +++ b/src/Orleans.Runtime/Catalog/StatelessWorkerGrainContext.cs @@ -43,12 +43,18 @@ internal partial class StatelessWorkerGrainContext : IGrainContext, IAsyncDispos public StatelessWorkerGrainContext( GrainAddress address, StatelessWorkerGrainTypeSharedContext sharedContext, - IGrainContextActivator innerActivator) + IGrainContextActivator innerActivator, + IConfigureGrainContext[] configureActions) { Address = address; _shared = sharedContext; _innerActivator = innerActivator; + foreach (var configure in configureActions) + { + configure.Configure(this); + } + if (_shared.RemoveIdleWorkers) { _inspectionTimer = new Timer( diff --git a/test/Orleans.Core.Tests/Runtime/GrainContextActivatorTests.cs b/test/Orleans.Core.Tests/Runtime/GrainContextActivatorTests.cs index bf9ef3d755e..5ace698222b 100644 --- a/test/Orleans.Core.Tests/Runtime/GrainContextActivatorTests.cs +++ b/test/Orleans.Core.Tests/Runtime/GrainContextActivatorTests.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using NSubstitute; using Orleans.Metadata; using Orleans.Runtime; @@ -27,7 +28,7 @@ [new TestConfigureGrainContextProvider(events)], private sealed class TestGrainContextActivatorProvider(IGrainContextActivator activator) : IGrainContextActivatorProvider { - public bool TryGet(GrainType grainType, out IGrainContextActivator result) + public bool TryGet(GrainType grainType, [NotNullWhen(true)] out IGrainContextActivator? result) { result = activator; return true; @@ -36,7 +37,10 @@ public bool TryGet(GrainType grainType, out IGrainContextActivator result) private sealed class TestConfigureGrainContextProvider(List events) : IConfigureGrainContextProvider { - public bool TryGetConfigurator(GrainType grainType, GrainProperties properties, out IConfigureGrainContext configurator) + public bool TryGetConfigurator( + GrainType grainType, + GrainProperties properties, + [NotNullWhen(true)] out IConfigureGrainContext? configurator) { configurator = new TestConfigureGrainContext(events); return true; diff --git a/test/Orleans.Runtime.Tests/GrainActivatorTests.cs b/test/Orleans.Runtime.Tests/GrainActivatorTests.cs index cf9d3407190..167363cecb5 100644 --- a/test/Orleans.Runtime.Tests/GrainActivatorTests.cs +++ b/test/Orleans.Runtime.Tests/GrainActivatorTests.cs @@ -135,7 +135,7 @@ public void Configure(GrainType grainType, GrainProperties properties, GrainType { // Selectively register this activator only for ExplicitlyRegisteredSimpleDIGrain types // Other grain types will continue using the default DI-based activator - if (_grainClassMap.TryGetGrainClass(grainType, out var grainClass) && grainClass.IsAssignableFrom(typeof(ExplicitlyRegisteredSimpleDIGrain))) + if (_grainClassMap.TryGetGrainClass(grainType, out var grainClass) && grainClass == typeof(ExplicitlyRegisteredSimpleDIGrain)) { shared.SetComponent(this); } @@ -168,7 +168,7 @@ public bool TryGetConfigurator( [NotNullWhen(true)] out IConfigureGrainContext? configurator) { if (grainClassMap.TryGetGrainClass(grainType, out var grainClass) - && grainClass.IsAssignableFrom(typeof(ExplicitlyRegisteredSimpleDIGrain))) + && grainClass == typeof(ExplicitlyRegisteredSimpleDIGrain)) { configurator = ActivationOrderingState.Instance; return true; @@ -182,14 +182,15 @@ public bool TryGetConfigurator( private sealed class ActivationOrderingState : IConfigureGrainContext { private int _armed; + private int _wasConfiguredAtConstruction; public static ActivationOrderingState Instance { get; } = new(); - public bool WasConfiguredAtConstruction { get; private set; } + public bool WasConfiguredAtConstruction => Volatile.Read(ref _wasConfiguredAtConstruction) != 0; public void Arm() { - WasConfiguredAtConstruction = false; + Volatile.Write(ref _wasConfiguredAtConstruction, 0); Volatile.Write(ref _armed, 1); } @@ -210,7 +211,9 @@ public void ObserveConstruction(IGrainContext context) return; } - WasConfiguredAtConstruction = context.GetComponent() is not null; + Volatile.Write( + ref _wasConfiguredAtConstruction, + context.GetComponent() is not null ? 1 : 0); } }