akkadotnet / akka.net

Canonical actor model implementation for .NET with local + distributed actors in C# and F#.
http://getakka.net
Other
4.69k stars 1.04k forks source link

Generic property injection on the purpose of plugins #533

Closed Horusiath closed 9 years ago

Horusiath commented 9 years ago

Since C# doesn't offer multiple inheritance (like Scala via traits) sometimes there is a need to bypass that problem by using composition over inheritance. Example is stashing with StashFactory.CreateStash: we use framework to automatically inject stash into actors marked with IActorStash interface.

This feature may be however used more often (we already could use it for example in logging and cluster actor initialization), and also be utilized by plugins. I've already found two use cases in one of the possible designs of Akka.Persistence plugin.

Should we provide some kind of a generic actor creation interceptor mechanism or should we dump the responsibility on shoulders of Akka users?

Aaronontheweb commented 9 years ago

This is an interesting idea - had a conversation with @rogeralsing the other day about setting up a generic way to do deep inspection of actors for performance monitoring purposes, so my head has been in a similar space recently.

Thinking about this a bit more... The way Akka.Cluster does this today is through subclassing the IActorRefProvider and specifying the ClusterActorRefProvider during configuration. Akka.Remote and Akka.Cluster are special cases though, as they bring in a whole bunch of functionality that fundamentally changes the way critical features like addressing, deployment, routing, and etc are performed.

What you're talking about, it seems to me, is something a little different - exposing a way of injecting new behaviors into local actors created using the default LocalActorRefProvider. Does that sound right?

We have the ActorSystemExtension stuff for doing some of this today - and that's how Akka.Cluster gets exposed to developers if they're not using built-in routers. But that puts the burden on the developer.

What it sounds like you're asking for is something similar to what the ControllerFactory or DependecyResolver do in ASP.NET MVC: change the way controllers are made, whether it's by subclassing the factory or using a DI framework.

I'd be open to putting something like that in, provided that we find a way to limit its scope to some specific contexts. I.E., I wouldn't want to expose a method to change the way mailboxes or paths or supervision are established for actors, but the ability to extend the actor creation process in order to provide an aspect-oriented way of helping with user-land concerns like monitoring or logging appeals to me.

Horusiath commented 9 years ago

What you're talking about, it seems to me, is something a little different - exposing a way of injecting new behaviors into local actors created using the default LocalActorRefProvider. Does that sound right?

I'd be open to putting something like that in, provided that we find a way to limit its scope to some specific contexts. I.E., I wouldn't want to expose a method to change the way mailboxes or paths or supervision are established for actors, but the ability to extend the actor creation process in order to provide an aspect-oriented way of helping with user-land concerns like monitoring or logging appeals to me.

Yes, that's what I meant. I'd like to create and manage a collection of injectors/behaviors, which would be applied to new actor after it's creation. Optionally also before it's deleted - for closing IDisposable properties of actors, since in my opinion if plugin initialized some disposable property for actor instance, it's also responsible for disposing it.

Example: take a look at CreateNewActorInstance method - we handle stash initialization as a special case. Instead we could invoke a pipeline of behaviors, that could initialize stuff like stash, logger etc. for this new instance.

Your example of performance monitoring would be more problematic, since it has to allow us using some "hooks" for Actor lifecycle events - stopping, starting and restarting. This could affect overall performance (you never know how long those method calls will take) but also is problematic from stability reasons (possible exceptions in "hooked-in" behaviors).

Aaronontheweb commented 9 years ago

@Horusiath yeah, monitoring is a special case due to how invasive it is. I'm probably going to introduce a set of system calls for monitoring internals (i.e. mailbox throughput, restarts of system actors, etc...) and add a plugin layer just like there is for loggers. Not sure on the approach yet.

However, to your point - I think there's merit in exploring the way we instantiate Loggers / Stashes within actors as a possible new generic and repurposable construction pipeline for local actors. What say you, @akkadotnet/developers ?

rogeralsing commented 9 years ago

Im all for it, I guess what we are saying is that we need some sort of post actor creation injection service? When an actor is created, we do property injection on it based on interfaces/attributes/convention for example? (Personally I dont like attribs/conventions but they are viable options)

Aaronontheweb commented 9 years ago

I'd go for something similar to the ActorSystem extension registry plus a generic marker interface for the actors.

So for instance, say we define these generic interfaces:

IActorPlugin {}

IActorPlugin<T> : IActorPlugin where T:new(){

}

IActorPluginProducer<T> where T:new(){
    T Construct();
}

And an ActorPlugin class that registers a producer instance to an ActorPluginRegistry:

public abstract ActorPluginBase<T> where T:new() {
    //Registers this plugin with the ActorPluginRegistry
    void Register(); 

    virtual IActorPluginProducer<T> Create();
}

When a user defines an Actor, they can decorate the actor with one or more plugin interfaces:

class FooActor: UntypedActor, IActorPlugin<Logger>{
    Logger MyLogger {get; set;}
}

And during the creation process, the registry checks for all instances of IActorPlugin on a given actor and caches a build plan for populating any field that matches T with whatever gets produced by the IActorPluginProducer.

Something along these lines is what I'm thinking.

Horusiath commented 9 years ago

Is anyone already on this or can I pick it up?

rogeralsing commented 9 years ago

Go for it. Would love some input from @HCanber on this too.

HCanber commented 9 years ago

I like it, however I have two things I'd like to discuss.

1 Created and Terminated are well defined concepts for an actor. An actor is created and terminated once, and only once, but the methods AfterActorCreated and BeforeActorTerminated are called several times during an actor's lifecycle. They are called every time the underlying actor instance (i.e. the class that the user writes) is created/terminated.

Unfortunately there's no term for the underlying instance. In our, as well as Akka JVM's code, the only time (as I know of) a user is exposed to the underlying instance is in TestActorRef which has a property called UnderlyingActor. So maybe we should use that term?

I.e. call the methods AfterUnderlyingActorCreated and BeforeUnderlyingActorTerminated.

2 Do we really need to check every actor instance?

public interface IActorProducerPlugin
{
  bool CanBeAppliedTo(ActorBase actor);
  ...
}

Or could we change this to:

public interface IActorProducerPlugin
{
  bool CanBeAppliedTo(Type actorType);
  ...
}

This would allow us to check the actor type once, build a pipeline with only the plugins applicable to the actor type and then store the pipeline (per type). Whenever AfterUnderlyingActorCreated and BeforeUnderlyingActorTerminated is to be called, we get the pipeline for the actor (type) and call the method, i.e. no need to call CanBeAppliedTo every time.

Horusiath commented 9 years ago

Considering 1. - I think that we should create some term for process of replacing underlying actor (if canonical Akka doesn't have one already) and use it for associated actions. Taken this description image my proposition would be Reincarnation - in this case methods could be called AfterActorCreatedAfterReincarnated and BeforeActorTerminatedBeforeReincarnated.

2. - I was also thinking about it. When I was developing this feature, I've thought that by providing an actor instance we may give users more possibilities. Now I see, that this is not the case, since this method will always be called on fresh actor instance. So here I agree with you.

HCanber commented 9 years ago

Reincarnation is the religious or philosophical concept that the soul or spirit, after biological death, can begin a new life in a new body. Wikipedia

So in order to be reincarnated the actor has had to die first which is not true the first time AfterActorCreated is called.

Reincarnation can be used to describe what happens during restarts.

rogeralsing commented 9 years ago

Akka documentation use the word incarnation for the underlying actor instance life. Would AfterIncarnated or AfterIncarnate work?

1. embodied in flesh; given a bodily ...

HCanber commented 9 years ago

Yep!

Aaronontheweb commented 9 years ago

I like @HCanber 's #2 suggestion - that'll help keep the costs of actor startup at a minimum.

Horusiath commented 9 years ago

Ok, i'll fix that one. Summarizing: replace single plugin pipeline into set of Actor type specific ones, and refactor current IActorProducerPlugin interface into something like this:

interface IActorProducerPlugin {
    bool CanBeAppliedTo(Type actorType);
    void AfterIncarnated (ActorBase actor, IActorContext context);
    void BeforeIncarnated (ActorBase actor, IActorContext context);
}
Aaronontheweb commented 9 years ago

So can we consider this feature "implemented" then?

rogeralsing commented 9 years ago

Done.