dotnet / orleans

Cloud Native application framework for .NET
https://docs.microsoft.com/dotnet/orleans
MIT License
10.15k stars 2.04k forks source link

Question - Heterogenous Silo configuration in runtime #4466

Closed tinpl closed 4 years ago

tinpl commented 6 years ago

Hello guys, is it possible to make Heterogenous Silos configuration by supplying the same build package with all grain implementations, but configuring which parts to host by AddApplicationParts or any similar way (but still referencing all grains)? We are just considering to supply the silo with some config and select required set of grains ( and dependent services to inject etc. ) to host when starting the silo. It will be very useful if you have the silo capable of hosting N feature-sets, and when the demand arises for some parts of functionality/services, you can choose for which features you need to allocate more resources (and resource configurations required) and start additional silos only for these, instead of running the one big silo.

DarkCow commented 6 years ago

I think placement directors are a better option... I made a role placement director. Then you decorate your Grain class with [RolePlacement( "fileserver" )]

You can use it by calling siloHostBuilder.AddPlacementDirector<RolePlacementStrategy, RolePlacementDirector>( )

Using this technique, I can change the roles of servers during runtime, and their cache will be fixed every min or so. I leave the implementation of m_SiloRoleRepository to you. But it is a service that returns SiloAddress <--> Supported Roles

    [Serializable]
    public class RolePlacementStrategy : PlacementStrategy
    {
        public string RoleName { get; private set; }

        internal RolePlacementStrategy( string roleName )
        {
            RoleName = roleName;
        }

        public override string ToString( )
        {
            return String.Format( $"RolePlacementStrategy(role={RoleName})" );
        }

        public override bool Equals( object obj )
        {
            if( obj is RolePlacementStrategy other )
                return other.RoleName == RoleName;
            else
                return false;
        }

        public override int GetHashCode( )
        {
            return GetType( ).GetHashCode( ) ^ RoleName.GetHashCode( );
        }
    }
    public class RolePlacementDirector : IPlacementDirector
    {
        public RolePlacementDirector( ISiloRoleRepository siloRoleRepository )
        {
            m_SiloRoleRepository = siloRoleRepository;
        }

        public virtual async Task<SiloAddress> OnAddActivation( PlacementStrategy strategy, PlacementTarget target, IPlacementContext context )
        {
            var allSilos = context.GetCompatibleSilos( target );
            var rolePlacementStrategy = (RolePlacementStrategy)strategy;
            var siloRoleInfos = await GetSiloRoleInfosAsync( );

            var silosInRole = siloRoleInfos
                .Where( r => r.Roles.Contains( rolePlacementStrategy.RoleName ) )
                .Join( allSilos,
                    r => r.IPAddress,
                    s => s.Endpoint,
                    ( r, s ) => new { SiloRoleInfo = r, SiloAddress = s } )
                .ToList( );

            if( silosInRole.Count == 0 )
                throw new SiloRoleNotFoundException( rolePlacementStrategy.RoleName );

            return silosInRole[ m_Random.Next( silosInRole.Count ) ].SiloAddress;
        }

        protected async Task<List<SiloRoleInfo>> GetSiloRoleInfosAsync( )
        {
            var siloRoleInfos = m_MemoryCache.Get<List<SiloRoleInfo>>( "SiloRoleInfo" );

            if( siloRoleInfos == null )
            {
                await m_CacheLock.WaitAsync( );

                try
                {
                    // Check twice
                    siloRoleInfos = m_MemoryCache.Get<List<SiloRoleInfo>>( "SiloRoleInfo" );

                    if( siloRoleInfos == null )
                    {
                        siloRoleInfos = await m_SiloRoleRepository.GetAsync( );

                        m_MemoryCache.Set( "SiloRoleInfo", siloRoleInfos, DateTime.Now.Add( ms_Interval ) );
                    }
                }
                finally
                {
                    m_CacheLock.Release( );
                }
            }

            return siloRoleInfos;
        }

        private Random m_Random = new Random( );
        private ISiloRoleRepository m_SiloRoleRepository;
        private MemoryCache m_MemoryCache = new MemoryCache( new MemoryCacheOptions( ) );
        private SemaphoreSlim m_CacheLock = new SemaphoreSlim( 1, 1 );
        private ILogger m_Logger = Log.ForContext<RolePlacementDirector>( );
        private static readonly TimeSpan ms_Interval = TimeSpan.FromMinutes( 1 );
    }
    /// <summary>
    /// Directs Orleans to only place new activations on a Silo supporting the Role
    /// </summary>
    [AttributeUsage( AttributeTargets.Class, AllowMultiple = false )]
    public sealed class RolePlacementAttribute : PlacementAttribute
    {
        public string Role { get; private set; }

        public RolePlacementAttribute( string role ) :
            base( new RolePlacementStrategy( role ) )
        {
            Role = role;
        }
    }
ReubenBond commented 6 years ago

I said I would comment on this, so here goes: There are a few ways to exclude grains from a silo. The placement solution mentioned by @DarkCow is one of them. Another is using GrainClassOptions.ExcludedGrainTypes:

siloBuilder.Configure<GrainClassOptions>(options.ExcludedGrainTypes.Add(typeof(MyGrain).FullName)));

I was originally thinking of another method, but it's quite ugly. You would replace the default ApplicationPartManager / IApplicationPartManager with your own implementation which filters out the unwanted grain classes from GrainClassFeature when it's being populated (in PopulateFeature). I don't recommend that approach.

tinpl commented 6 years ago

Thank you for your responses, both of them do what we want to achieve!

ApplicationPartManager seems to be more conventional way to configure the silo at the startup, if you don't want to change hosted types in the future. Kind of you are building the silo here, so that should be the place to define what should it host. Maybe adding one more class (similar to AssemblyPart),something like GrainPart: IAssemblyPart { ... }, which can be added like builder.AddApplicationPart(typeof(GrainClass)) will do the work. So if you want to just put in the assembly, use one method. If you need more control -> add Grains explicitly one by one (or IEnumerable<IAssemblyPart> of them, probably, this will be more common usage). Excluded Grain types approach functionally seems to be good one, but intuitively you expect to tell what to Include, not what to Exclude (so if I don't say to Include this grain/assembly of grains -> it shouldn't be here). imho, seems to be slightly misleading. Placement director seems to be the most flexible one, with the possibility to add additional rules etc.

Seems my solution will look like:

  1. Set basic capabilities for a silo from config using ExcludedGrainTypes (when deploying you just don't want to have some kind of Grains to be on this silo because of this instance resource constraints)
  2. Arrange some custom placement strategy using the PlacementDirector, based on the Silo's capabilities in terms of hosted 'features' and results of monitoring/metrics, or something similar.

Any drawbacks in this approach? Or maybe places I should consider to put more attention in, because of possible reliability/performance issues?

ReubenBond commented 4 years ago

Apologies for the extremely slow response. Your points both look fine to me. Closing