Dec 6, 2018

[Golang] protoactor-go 201: Use plugins to add behaviors to an actor

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.