Orleans: Orleans Facet System

Created on 14 Jul 2017  Â·  11Comments  Â·  Source: dotnet/orleans

As Orleans has grown and new features have been introduced, it has become clear that formalizing patterns for adding new features to Orleans will improved the maintainability, stability, and testability of the framework. Each new feature we’ve added has been introduced in slightly different ways with different advantages and disadvantages. For instance, grain storage was introduced via a base class, limiting its composability, while streams were introduced using grain extensions making it nearly impossible for third party developers to build their own stream providers. The variety of patterns, which mostly do similar things, have, over time, added significant complexity to maintaining, testing and improving Orleans. To address these problems, we’re introducing an extensibility pattern we hope to use for most if not all new features added in the future. Since all the good names for such generic extensibility patterns have been taken, we’re calling this Orleans Facet System. This system allows ‘facets’ of the Orleans framework to be exposed to the application layer via grains.

Facets are Orleans features that can be injected into the grain via grain construction and take part in the grain’s lifecycle. They are very similar to dependency injected services scoped to a grain, with the additional ability to asynchronously take part in stages of a grain’s lifecycle.

To illustrate how this is expected to look, let’s explore what grain state might look like as a facet rather than via inheritance, as it currently is.

Currently, to use grain state storage, one would inherit from a base class that might look something like:

[StorageProvider("Blob")]
public class MyGrain : Grain<MyState>, IMyGrain
{
}

Where the base class exposes the following properties/calls.

MyState State { get; set; }
Task ReadStateAsync();
Task WriteStateAsync();
Task ClearStateAsync();

Exposing similar state storage capabilities via a facet may look something like:

public class MyGrain : Grain, IMyGrain
{
    private readonly IPersistentState<MyState> state;
    public MyGrain(
        [PersistentStateStorage("Blob", "MyState")]
        IPersistentState<MyState> state,
    {
        this.state = state;
    }
}

Where the IPersistentState interface exposes the following properties/calls.

public interface IPersistentState<TState>
{
    TState State { get; }
    Task ReadStateAsync();
    Task WriteStateAsync();
    Task ClearStateAsync();
}

The infrastructure provided by Grain\

By exposing Orleans features in this way, we, and the community, can create or integrate narrowly focused features into Orleans in a way that allows grain developers to compose the features in a more flexible manner. Consider a stream consumer grain that stores stream recovery state and user data in separate storage accounts. Stream processing and separate storage logic may be composed into a single grain as follows.

public class MyGrain : Grain, IMyGrain
{
    private readonly IPersistentState<StreamState> streamState;
    private readonly IPersistentState<UserState> userState;
    private readonly IStreamConsumer<UserData> streamConsumer;
    public MyGrain(
        [PersistentStateStorage("Blob", "StreamState")]
        IPersistentState<StreamState> streamState,
        [PersistentStateStorage("Table", "UserState")]
        IPersistentState<UserState> userState,
        [StreamProvider("AzureQueue")]
        IStreamConsumer<UserData> streamConsumer)
    {
        this.streamState = streamState;
        this.userState = userState;
        this.streamConsumer = streamConsumer;
    }
}

The initial minimal capability for Orleans Facets is being introduced in PR "Preliminary Orleans Facet System implementation #3208"

enhancement

Most helpful comment

@halfnelson

Now that client isn't static, could it also benefit from same facet system?

The technical details and limitations of the system are still being worked out, and the primary target of this system is grain developers. I'm unaware of any effort being made to extend this to the client, but it does seem like a good idea.

>

How would this facet system interact with grain extensions?

Currently grain extensions are internal only. The plan is for them to be made available for public use. Facets (including application developed facets) will have the ability to register grain extensions to a grain in its lifecycle prior to the OnActivateAsync call. This will enable more advanced feature development like alternative streaming infrastructure, or distributed TPL Dataflows.

All 11 comments

This is good for grains, will it be possible to also do this for clients. For example for a client to consume a stream, it gets "extended" with streamConsumer. Now that client isn't static, could it also benefit from same facet system?

How would this facet system interact with grain extensions?

@halfnelson

Now that client isn't static, could it also benefit from same facet system?

The technical details and limitations of the system are still being worked out, and the primary target of this system is grain developers. I'm unaware of any effort being made to extend this to the client, but it does seem like a good idea.

>

How would this facet system interact with grain extensions?

Currently grain extensions are internal only. The plan is for them to be made available for public use. Facets (including application developed facets) will have the ability to register grain extensions to a grain in its lifecycle prior to the OnActivateAsync call. This will enable more advanced feature development like alternative streaming infrastructure, or distributed TPL Dataflows.

to implement streams that can be subscribed from clients, then clients will probably need facets or at least access to the extension system.

Update on the design

While the goals of the system remain the same, we’re changing how this capability will be exposed to users. Rather than facets being injected via constructor injection, we’ve opted to use property injection instead.
So the grains from the example in the description would instead look like:

public class MyGrain : Grain, IMyGrain
{
    [PersistentStateStorage("Blob", "MyState")]
    public IPersistentState<MyState> State { get;set; }
}

And

public class MyGrain : Grain, IMyGrain
{
    [PersistentStateStorage("Blob", "StreamState")]
    public IPersistentState<StreamState> StreamState { get;set; }
    [PersistentStateStorage("Table", "UserState")]
    public IPersistentState<UserState> UserState { get;set; };
    [StreamProvider("AzureQueue")]
    public IStreamConsumer<UserData> StreamConsumer { get;set; };
}

The facet system will perform property injection to wire up the facets after grain construction and prior to calling OnActivateAsync.

The initial implementation "Preliminary Orleans Facet System implementation #3208" will be updated to reflect this decision.

EDIT: Closed "Preliminary Orleans Facet System implementation #3208". Will introduce property injected version in separate PR.

Initial property injected version: Orleans facet property injection #3247

The team discussed this further (along with input from the community), and came to the following conclusions:

  1. Supporting custom grain activator to the degree that all runtime features, facets or not, magically work is a non-goal. We expect the vast majority of Orleans users to use the default grain activator. We can document the requirements and constraints for custom grain activators that their users will have to follow in order to have access to all features of Orleans.
  2. Facets are a composition mechanism for Orleans features. They are specifically intended for services that interact with the Orleans runtime in some way, e.g. participating in grain lifecycle, installing a grain extension or a call filter, or performing background tasks in the context of the grain.
  3. The key priority is that facets let Orleans users consume features provided by us in a clean and intuitive way. It’s almost as important for facets to support community or custom built features for Orleans. Facets are not intended as an open-ended general purpose extensibility mechanism.
  4. Property injection in ASP.NET is not a good guidance for us because it is only used for a very limited and constrained purpose targeting a small percentage of their customers.
  5. We will build features for facets with a flexible factory-based pattern. Those underlying factories could be used by some users directly for a greater flexibility. However, we expect the majority of Orleans users to consume facet features via attributed code artifacts to encourage a more declarative, intuitive, and discoverable style of their consumption.

With the combination of the previous points, we think that we could get back to the constructor injected facets that everyone seems to prefer in general, as the concerns that led us to switching to property injection would be negated.

To be clear, we've decided to expose features via factories, and support a declarative programming model (using attributes) to inject Orleans features into grains at construction time. The example patterns proposed in this issue's description will be supported along with direct use of factories, as seen below.

Attribute Examples (from issue description) - primary pattern

public class MyGrain : Grain, IMyGrain
{
    private readonly IPersistentState<MyState> state;
    public MyGrain(
        [PersistentStateStorage("Blob", "MyState")]
        IPersistentState<MyState> state)
    {
        this.state = state;
    }
}

and

public class MyGrain : Grain, IMyGrain
{
    private readonly IPersistentState<StreamState> streamState;
    private readonly IPersistentState<UserState> userState;
    private readonly IStreamConsumer<UserData> streamConsumer;
    public MyGrain(
        [PersistentStateStorage("Blob", "StreamState")]
        IPersistentState<StreamState> streamState,
        [PersistentStateStorage("Table", "UserState")]
        IPersistentState<UserState> userState,
        [StreamProvider("AzureQueue")]
        IStreamConsumer<UserData> streamConsumer)
    {
        this.streamState = streamState;
        this.userState = userState;
        this.streamConsumer = streamConsumer;
    }
}

Factory Examples:

public class MyGrain : Grain, IMyGrain
{
    private readonly IPersistentState<MyState> state;
    public MyGrain(
        INamedPersistentStateFactory stateFactory)
    {
        this.state = stateFactory.Create<MyState>("Blob", "MyState");
    }
}

and

public class MyGrain : Grain, IMyGrain
{
    private readonly IPersistentState<StreamState> streamState;
    private readonly IPersistentState<UserState> userState;
    private readonly IStreamConsumer<UserData> streamConsumer;
    public MyGrain(
        INamedPersistentStateFactory stateFactory,
        INamedStreamConsumerFactory streamConsumerFactory)
    {
        this.streamState = stateFactory.Create<StreamState>("Blob", "StreamState");
        this.userState = stateFactory.Create<UserState>("Table", "UserState");
        this.streamConsumer = streamConsumerFactory.Create<UserData>("AzureQueue");
    }
}

EDIT: Included examples from Issue description to ensure it was clear that those are the primary patterns we're supporting, but that we are also exposing the features via factories.

I'll be reintroducing an Orleans Facet System PR using constructor injection after the lifecycle PR is resolved. I will not be reopening Preliminary Orleans Facet System implementation #3208, as it's history should be preserved for transparency on this decision, and would clutter the review of the (hopefully) final system.

@jason-bragg to be clear, we are not using attributes on ctor-injected fields anymore. Instead of parameters, we would be injecting factories instead of the target type and then, with those factories, we would actually create the instance of the type we want, right?

If that is the case, something is not clear to me... In your example:

private readonly IPersistentState<UserState> userState;
...
INamedPersistentStateFactory<UserState> userStateFactory
...
this.userState = userStateFactory.Create("Table", "UserState")

That means that factory.Create() is a synchronous method and will run in the ctor. For instantiation, it is OK but, for the persistent state, how would the state be loaded? Today we have the ReadStateAsync() called before the OnActivateAsync() which happens (obviously) after the ctor().

We can ask the user to manually load and hydrate the state object in OnActivateAsync() but I'm not sure whether it is a good idea.

In the original design we have something like this in the ctor:

PersistentStateStorage("Blob", "MyState")]
        IPersistentState<MyState> state

I would assume that since it is not a factory, the state was loaded and the object hydrated at the grain activator and just injected when the grain ctor was called.

Can you elaborate more on that?

Thanks!

@galvesribeiro, I apologize for being unclear. We're primarily supporting declarative attributes from the original issue, but we are also supporting factories.

_The example patterns proposed in this issue's description_ will be supported along with direct use of factories

I updated the comment to make it more clear.

As far as the asynchronous nature of some of the behaviors, the factories will be responsible for wiring the facets up to the grain's lifecycle, where asynchronous behaviors will be performed. The facet creation will likely always be synchronous.

The new facet system implementation using declarative attributes and factory injection via the constructor can be found in "Orleans Facet System initial infrastructure #3279"

Thanks @jason-bragg! I'm looking forward to try it :)

Was this page helpful?
0 / 5 - 0 ratings