diff --git a/src/Orleans.Core/Runtime/Constants.cs b/src/Orleans.Core/Runtime/Constants.cs index df8ae2ac671..3c3e717ae3a 100644 --- a/src/Orleans.Core/Runtime/Constants.cs +++ b/src/Orleans.Core/Runtime/Constants.cs @@ -37,6 +37,7 @@ internal class Constants public static readonly GrainId MembershipOracleId = GrainId.GetSystemTargetGrainId(15); public static readonly GrainId TypeManagerId = GrainId.GetSystemTargetGrainId(17); public static readonly GrainId FallbackSystemTargetId = GrainId.GetSystemTargetGrainId(19); + public static readonly GrainId StartupTaskSystemTargetId = GrainId.GetSystemTargetGrainId(20); public static readonly GrainId DeploymentLoadPublisherSystemTargetId = GrainId.GetSystemTargetGrainId(22); public static readonly GrainId MultiClusterOracleId = GrainId.GetSystemTargetGrainId(23); public static readonly GrainId ClusterDirectoryServiceId = GrainId.GetSystemTargetGrainId(24); diff --git a/src/Orleans.Runtime/Hosting/DefaultSiloServices.cs b/src/Orleans.Runtime/Hosting/DefaultSiloServices.cs index 802dd7828d9..458b2d326b7 100644 --- a/src/Orleans.Runtime/Hosting/DefaultSiloServices.cs +++ b/src/Orleans.Runtime/Hosting/DefaultSiloServices.cs @@ -260,6 +260,9 @@ internal static void AddDefaultServices(HostBuilderContext context, IServiceColl services.TryConfigureFormatter(); services.AddTransient(); + + services.AddSingleton(); + } } } \ No newline at end of file diff --git a/src/Orleans.Runtime/Lifecycle/IStartupTask.cs b/src/Orleans.Runtime/Lifecycle/IStartupTask.cs new file mode 100644 index 00000000000..ebd1f50f037 --- /dev/null +++ b/src/Orleans.Runtime/Lifecycle/IStartupTask.cs @@ -0,0 +1,18 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace Orleans.Runtime +{ + /// + /// Defines an action to be taken during silo startup. + /// + public interface IStartupTask + { + /// + /// Called after the silo has started. + /// + /// The cancellation token which is canceled when the method must abort. + /// A representing the work performed. + Task Execute(CancellationToken cancellationToken); + } +} diff --git a/src/Orleans.Runtime/Lifecycle/SiloBuilderStartupExtensions.cs b/src/Orleans.Runtime/Lifecycle/SiloBuilderStartupExtensions.cs new file mode 100644 index 00000000000..2d2b0233c7c --- /dev/null +++ b/src/Orleans.Runtime/Lifecycle/SiloBuilderStartupExtensions.cs @@ -0,0 +1,129 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +using Microsoft.Extensions.DependencyInjection; + +using Orleans.Runtime; +using Orleans.Runtime.Scheduler; + +namespace Orleans.Hosting +{ + /// + /// The silo builder startup extensions. + /// + public static class SiloBuilderStartupExtensions + { + /// + /// Adds a startup task to be executed when the silo has started. + /// + /// + /// The builder. + /// + /// + /// The stage to execute the startup task, see values in . + /// + /// + /// The startup task type. + /// + /// + /// The provided . + /// + public static ISiloHostBuilder AddStartupTask( + this ISiloHostBuilder builder, + int stage = SiloLifecycleStage.SiloActive) + where TStartup : class, IStartupTask + { + return builder.AddStartupTask((sp, ct) => ActivatorUtilities.GetServiceOrCreateInstance(sp).Execute(ct), stage); + } + + /// + /// Adds a startup task to be executed when the silo has started. + /// + /// + /// The builder. + /// + /// + /// The startup task. + /// + /// + /// The stage to execute the startup task, see values in . + /// + /// + /// The provided . + /// + public static ISiloHostBuilder AddStartupTask( + this ISiloHostBuilder builder, + IStartupTask startupTask, + int stage = SiloLifecycleStage.SiloActive) + { + return builder.AddStartupTask((sp, ct) => startupTask.Execute(ct), stage); + } + + /// + /// Adds a startup task to be executed when the silo has started. + /// + /// + /// The builder. + /// + /// + /// The startup task. + /// + /// + /// The stage to execute the startup task, see values in . + /// + /// + /// The provided . + /// + public static ISiloHostBuilder AddStartupTask( + this ISiloHostBuilder builder, + Func startupTask, + int stage = SiloLifecycleStage.SiloActive) + { + builder.ConfigureServices(services => + services.AddTransient>(sp => + new StartupTask( + sp, + sp.GetRequiredService(), + sp.GetRequiredService(), + startupTask, + stage))); + return builder; + } + + /// + private class StartupTask : ILifecycleParticipant + { + private readonly IServiceProvider serviceProvider; + private readonly OrleansTaskScheduler scheduler; + private readonly StartupTaskSystemTarget schedulingTarget; + private readonly Func startupTask; + + private readonly int stage; + + public StartupTask( + IServiceProvider serviceProvider, + OrleansTaskScheduler scheduler, + StartupTaskSystemTarget schedulingTarget, + Func startupTask, + int stage) + { + this.serviceProvider = serviceProvider; + this.scheduler = scheduler; + this.schedulingTarget = schedulingTarget; + this.startupTask = startupTask; + this.stage = stage; + } + + /// + public void Participate(ISiloLifecycle lifecycle) + { + lifecycle.Subscribe( + this.stage, + cancellation => this.scheduler.QueueTask( + () => this.startupTask(this.serviceProvider, cancellation), + this.schedulingTarget.SchedulingContext)); + } + } + } +} \ No newline at end of file diff --git a/src/Orleans.Runtime/Silo/Silo.cs b/src/Orleans.Runtime/Silo/Silo.cs index 22e3d574d60..9b369634835 100644 --- a/src/Orleans.Runtime/Silo/Silo.cs +++ b/src/Orleans.Runtime/Silo/Silo.cs @@ -361,8 +361,11 @@ await scheduler.QueueAction(catalog.Start, catalog.SchedulingContext) // SystemTarget for provider init calls this.fallbackScheduler = Services.GetRequiredService(); - RegisterSystemTarget(fallbackScheduler); + + // SystemTarget for startup tasks + var startupTaskTarget = Services.GetRequiredService(); + RegisterSystemTarget(startupTaskTarget); } private Task OnRuntimeInitializeStart(CancellationToken ct) @@ -869,5 +872,14 @@ public FallbackSystemTarget(ILocalSiloDetails localSiloDetails, ILoggerFactory l { } } + + // A dummy system target for scheduling startup tasks. + internal class StartupTaskSystemTarget : SystemTarget + { + public StartupTaskSystemTarget(ILocalSiloDetails localSiloDetails, ILoggerFactory loggerFactory) + : base(Constants.StartupTaskSystemTargetId, localSiloDetails.SiloAddress, loggerFactory) + { + } + } } diff --git a/test/Tester/StartupTaskTests.cs b/test/Tester/StartupTaskTests.cs new file mode 100644 index 00000000000..a101465c81b --- /dev/null +++ b/test/Tester/StartupTaskTests.cs @@ -0,0 +1,82 @@ +using System.Threading; +using System.Threading.Tasks; + +using Microsoft.Extensions.DependencyInjection; + +using Orleans; +using Orleans.Hosting; +using Orleans.Runtime; +using Orleans.TestingHost; + +using TestExtensions; + +using UnitTests.GrainInterfaces; + +using Xunit; + +namespace DefaultCluster.Tests +{ + [TestCategory("BVT"), TestCategory("Lifecycle")] + public class StartupTaskTests : IClassFixture + { + public class Fixture : BaseTestClusterFixture + { + protected override void ConfigureTestCluster(TestClusterBuilder builder) + { + builder.ConfigureHostConfiguration(TestDefaultConfiguration.ConfigureHostConfiguration); + builder.AddSiloBuilderConfigurator(); + } + + private class StartupTaskSiloConfigurator : ISiloBuilderConfigurator + { + public void Configure(ISiloHostBuilder hostBuilder) + { + hostBuilder.AddStartupTask(); + hostBuilder.AddStartupTask( + async (services, cancellation) => + { + var grainFactory = services.GetRequiredService(); + var grain = grainFactory.GetGrain(98052); + + // Modify "A" to validate ordering of events. + await grain.SetA(await grain.GetA() * 2); + }); + } + } + } + + public class CallGrainStartupTask : IStartupTask + { + private readonly IGrainFactory grainFactory; + + public CallGrainStartupTask(IGrainFactory grainFactory) + { + this.grainFactory = grainFactory; + } + + public async Task Execute(CancellationToken cancellationToken) + { + var grain = this.grainFactory.GetGrain(98052); + await grain.SetA(1331); + } + } + + private readonly Fixture fixture; + + public StartupTaskTests(Fixture fixture) + { + this.fixture = fixture; + } + + /// + /// Ensures that startup tasks can call grains and are executed in the registered order. + /// + [Fact] + public async Task StartupTaskCanCallGrains() + { + var grain = this.fixture.GrainFactory.GetGrain(98052); + var value = await grain.GetA(); + Assert.Equal(2662, value); + } + } +}