The previous article, “How middleware works to intercept incoming and outgoing messages”, introduced how middlewares can be used to add behaviors without modifying an actor’s implementation. This is a good AOP-ish approach for multiple types of actors to execute a common procedure such as logging on message receiving/sending. A plugin mechanism is implemented on top of this middleware mechanism to run a specific task on actor initialization and message reception to enhance an actor’s ability. This article covers the implementation of this plugin feature and how this can be used.
Under the Hood
Implementing plugin is as easy as fulfilling the below plugin.plugin
interface.
type plugin interface {
OnStart(actor.Context)
OnOtherMessage(actor.Context, interface{})
}
When a plugin.plugin
implementation is passed to plugin.Use()
, this wraps the given plugin and return in a form of actor.InboundMiddleware
so this can be set to actor.Props
as a middleware.
func Use(plugin plugin) func(next actor.ActorFunc) actor.ActorFunc {
return func(next actor.ActorFunc) actor.ActorFunc {
fn := func(context actor.Context) {
switch msg := context.Message().(type) {
case *actor.Started:
plugin.OnStart(context)
default:
plugin.OnOtherMessage(context, msg)
}
next(context)
}
return fn
}
}
As shown in the above code fragment, plugin.OnStart
is called on actor initialization; plugin.OnOtherMessage
is called on subsequent message receptions. A developer may initialize plugin on plugin.OnStart
so its logic can run on other message receptions. Remember that next(context)
is called at the end of its execution so the actor’s actor.Receive()
is called after the plugin logic runs. A minimal implementation can be somewhat like below:
Example
A good example should be a passivation plugin provided by protoactor-go itself. This is a plugin that enables an idle actor to stop when no message comes in for a certain amount of time. Such plugin comes in handy when a developer employs cluster grain architecture because a grain actor is automatically initialized on first message reception and this lives forever without such self destraction mechanism. When a message is received after the destraction, another grain actor is automatically created. This initializes a timer on actor initialization, resets a timer on every message reception and stops the actor when timer ticks.
One important thing to mention here is that this plugin makes an extra effort to let an actor implement plugin.PassivationAware
by embedding plugin.PassivationHolder
in actor struct so a developer does not have to implement plugin.PassivationAware
by oneself.
Thanks for that effort, an actor implementation can be as simple as below. This is obvious that, because the passivation implementation itself is implemented by embedded plugin.PassivationHolder
, MyActor
developer can separate the passivation procedure and concentrate on her own business.
type MyActor struct {
// Implement plugin.PassivationAware by embedding its default implementation: plugin.PassivationHolder
PassivationHolder
}
func (state *MyActor) Receive(context actor.Context) {
switch context.Message().(type) {
// Do its own business
}
}
Conclusion
To add a pluggable behavior to an actor, a developer can provde a plugin by implementing plugin.plugin
interface. By defining core interface and its embeddable default implementation of the plugin, it is quite easier to separate the areas of responsibility of a plugin and an actor.