Edit

Share via


Grain services

Grain services are remotely accessible, partitioned services for supporting grain functionality. Each instance of a grain service is responsible for some set of grains. Those grains can get a reference to the grain service currently responsible for servicing them by using a GrainServiceClient.

Grain services exist to support cases where responsibility for servicing grains should be distributed around the Orleans cluster. For example, Orleans Reminders are implemented using grain services: each silo handles reminder operations for a subset of grains and notifies those grains when their reminders fire.

You configure grain services on silos. They initialize when the silo starts, before the silo completes initialization. They aren't collected when idle; instead, their lifetimes extend for the lifetime of the silo itself.

Create a grain service

A GrainService is a special grain: it has no stable identity and runs in every silo from startup to shutdown. Implementing an IGrainService interface involves several steps.

  1. Define the grain service communication interface. Build the interface of a GrainService using the same principles you use for building a grain interface.

    public interface IDataService : IGrainService
    {
        Task MyMethod();
    }
    
  2. Create the DataService grain service. It's helpful to know that you can also inject an IGrainFactory so you can make grain calls from your GrainService.

    [Reentrant]
    public class DataService : GrainService, IDataService
    {
        readonly IGrainFactory _grainFactory;
    
        public DataService(
            IServiceProvider services,
            GrainId id,
            Silo silo,
            ILoggerFactory loggerFactory,
            IGrainFactory grainFactory)
            : base(id, silo, loggerFactory)
        {
            _grainFactory = grainFactory;
        }
    
        public override Task Init(IServiceProvider serviceProvider) =>
            base.Init(serviceProvider);
    
        public override Task Start() => base.Start();
    
        public override Task Stop() => base.Stop();
    
        public Task MyMethod()
        {
            // TODO: custom logic here.
            return Task.CompletedTask;
        }
    }
    
    [Reentrant]
    public class DataService : GrainService, IDataService
    {
        readonly IGrainFactory _grainFactory;
    
        public DataService(
            IServiceProvider services,
            IGrainIdentity id,
            Silo silo,
            ILoggerFactory loggerFactory,
            IGrainFactory grainFactory)
            : base(id, silo, loggerFactory)
        {
            _grainFactory = grainFactory;
        }
    
        public override Task Init(IServiceProvider serviceProvider) =>
            base.Init(serviceProvider);
    
        public override Task Start() => base.Start();
    
        public override Task Stop() => base.Stop();
    
        public Task MyMethod()
        {
            // TODO: custom logic here.
            return Task.CompletedTask;
        }
    }
    
  3. Create an interface for the GrainServiceClient<TGrainService>GrainServiceClient that other grains will use to connect to the GrainService.

    public interface IDataServiceClient : IGrainServiceClient<IDataService>, IDataService
    {
    }
    
  4. Create the grain service client. Clients typically act as proxies for the grain services they target, so you usually add a method for each method on the target service. These methods need to get a reference to the target grain service so they can call into it. The GrainServiceClient<T> base class provides several overloads of the GetGrainService method that can return a grain reference corresponding to a GrainId, a numeric hash (uint), or a SiloAddress. The latter two overloads are for advanced cases where you want to use a different mechanism to map responsibility to hosts or address a host directly. In the sample code below, we define a property, GrainService, which returns the IDataService for the grain calling the DataServiceClient. To do that, we use the GetGrainService(GrainId) overload in conjunction with the CurrentGrainReference property.

    public class DataServiceClient : GrainServiceClient<IDataService>, IDataServiceClient
    {
        public DataServiceClient(IServiceProvider serviceProvider)
            : base(serviceProvider)
        {
        }
    
        // For convenience when implementing methods, you can define a property which gets the IDataService
        // corresponding to the grain which is calling the DataServiceClient.
        private IDataService GrainService => GetGrainService(CurrentGrainReference.GrainId);
    
        public Task MyMethod() => GrainService.MyMethod();
    }
    
  5. Inject the grain service client into the other grains that need it. The GrainServiceClient isn't guaranteed to access the GrainService on the local silo. Your command could potentially be sent to the GrainService on any silo in the cluster.

    public class MyNormalGrain: Grain<NormalGrainState>, INormalGrain
    {
        readonly IDataServiceClient _dataServiceClient;
    
        public MyNormalGrain(
            IGrainActivationContext grainActivationContext,
            IDataServiceClient dataServiceClient) =>
                _dataServiceClient = dataServiceClient;
    }
    
  6. Configure the grain service and grain service client in the silo. You need to do this so the silo starts the GrainService.

    (ISiloHostBuilder builder) =>
        builder.ConfigureServices(
            services => services.AddGrainService<DataService>()
                                .AddSingleton<IDataServiceClient, DataServiceClient>());
    

Additional notes

There's an extension method, GrainServicesSiloBuilderExtensions.AddGrainService, used to register grain services.

services.AddSingleton<IGrainService>(
    serviceProvider => GrainServiceFactory(grainServiceType, serviceProvider));

The silo fetches IGrainService types from the service provider when starting (see orleans/src/Orleans.Runtime/Silo/Silo.cs):

var grainServices = this.Services.GetServices<IGrainService>();

The Microsoft.Orleans.Runtime NuGet package should be referenced by the GrainService project.

The Microsoft.Orleans.OrleansRuntime NuGet package should be referenced by the GrainService project.

For this to work, you must register both the service and its client. The code looks something like this:

var builder = new HostBuilder()
    .UseOrleans(c =>
    {
        c.AddGrainService<DataService>()  // Register GrainService
        .ConfigureServices(services =>
        {
            // Register Client of GrainService
            services.AddSingleton<IDataServiceClient, DataServiceClient>();
        });
    })