Nov 23, 2018

[Golang] protoactor-go 101: How actor.Future works to synchronize concurrent task execution

Fine-grained actors concentrate on their own tasks and communicate with others to accomplish a bigger task as a whole. That is how a well-designed actor system works. Because each actor handles a smaller part of an incoming task, pass it to another and then proceed to work on the next incoming task, actors can efficiently work in a concurrent manner to accomplish more tasks in the same amount of time. To pass a result of one actor’s job execution to another or to execute a task when another actor’s execution is done, actor.Future mechanism comes in handy.

Introducing Future

The basic idea of “future” in this context is quite similar to that of future and promise implemented by many modern programming languages to coordinate concurrent executions.

For example, Future’s Javadoc reads as below:

AFuturerepresents the result of an asynchronous computation. Methods are provided to check if the computation is complete, to wait for its completion, and to retrieve the result of the computation.

In protoactor-go, actor.Future provides methods to wait for destination actor’s response in a blocking manner, to pipe one actor’s response to another in a non-blocking manner and to execute a callback function when destination actor’s response arrives.

How this works under the hood

The implementation of protoactor-go’s Future mechanism is composed of actor.Future and actor.futureProcess, where actor.Future provides common Future methods while actor.futureProcess wraps actor.Future and works as a actor.Process. A developer may call Context.RequestFuture() or PID.RequestFuture() instead of commonly used Context.Request() or PID.Request() to acquire actor.Future that represents a non-determined result.

In those method calls, actor.NewFuture() is called with preferred timeout duration as an argument. In actor.NewFuture(), actor.Future and its wrapper – actor.futureProcess – are both constructed, actor.futureProcess is registered to protoactor-go’s internal process registry for a later reference and actor.Future is returned.

As depicted in the below code fragment, actor.Future’s actor.PID is set as a Sender of the requesting message. So when the receiving actor responds to the sender, the response is actually sent to the actor.Future’s actor.PID. When actor.Future receives the response, the result of the Future is set and becomes available to the subscriber.

func (ctx *localContext) RequestFuture(pid *PID, message interface{}, timeout time.Duration) *Future {  
   future := NewFuture(timeout)  
   env := &MessageEnvelope{  
      Header:  nil,  
  Message: message,  
  Sender:  future.PID(),  
  }  
   ctx.sendUserMessage(pid, env)  
  
   return future  
}

The usage of the actor.Future returned by those methods are covered in later sections with detailed example codes. All example codes are available at github.com/oklahomer/protoactor-go-future-example.

Future.Wait() / Future.Result()

To wait until the execution times out or the destination actor responds, use Future.Wait() or Future.Result(). They both internally call a blocking private method, Future.wait(), to block till the preconfigured timeout duration passes or the execution completes. The only difference is whether to return the result of the computation; Future.Wait() simply waits for completion just like WaitGroup.Wait() while Future.Result() waits for completion and additionally returns the result ot the computation as its name suggests.

These methods are useful to retrieve the destination actors response and proceed own logic but are not usually preferred because the idea of such synchronous execution conflicts with the nature of actor model: concurrent computation.

Future.PipeTo()

While Future.Wait() and Future.Result() block until timeout or task completion, Future.PipeTo() asynchronously sends the result of computation to another actor. This can be a powerful tool when only the origination actor knows which actor should receive the result of a worker actor’s task; Actor A delegates a task to worker actor B but B does not know to what actor to pass the result message to. One important thing is that the message is transfered to the destination actor when and only when the comptation completes before it times out. Otherwise the response is sent to the dead letter mailbox.
Becausee this works in an asynchronous manner, origination actor can handle incoming messages right after dispatching tasks to worker actors no matter how long the worker actors take to respond.

Context.AwaitFuture()

The task execution is done in an asynchronous manner like Future.PipeTo but, because this can refer to contextual information, a callback function is still called even when the computation times out. Context.Message() contains the same message as when the origination actor’s Actor.Receive() is called so a developer does not have to add a workaround to copy the message to refer from the callback function.

Conclusion

As described in above sections, Future provides various methods to synchronize concurrent execution. While concurrent execution is the core of actor model, these come in handy to synchronize concurrent execution with minimal cost.