Scala actors design patterns - part 2

 In the previous post, I discussed how to handle sending data from an actor in response to a message (request) from another. This time I want to discuss how to handle that response.

 

In the simple case, an actor sends a request, and awaits a response:

 

actor {
  anotherActor 
! Request
  react 
{
    case Response 
=> ....
  
}
}

 

This is fine for this case, but what if we’re expecting interleaving response messages? Imagine a fork-join type of solution. An actor forks a task to smaller ones by sending requests to several actors and then waits for the responses from them.

 

actor {
  for 
((i, anActor) <- actors.zipWithIndex) taskActor ! Request(task(i), i)
  react 
{
    case Response
(i) => // join task i's result
  
}
}

 

What we have required from the message protocol is to send back the index of the requested task. The index is not used in the actor handling the task, but is required by the requester.

 

What if we want to implement the forking and joining in another way? E.g., there’s a meta-data associated with each task that is referenced by an ID, not a simple index.  Should we change the protocol?

 

In cases like this, the pattern is to create the Request-Response protocol so that the Request message has an opaque argument that is put in the response the way it came. This sort of data is sometimes called a cookie

 

case class Requset[T](<other arguments>, cookie: T)
case class Response
[T](<other arguments>, cookie: T)

 

This can be combined with the pattern from the previous post:

 

case class Requset[T, U](<other arguments>, extract: T => U)
type MyRequest 
= Request[Result, MyResponse]
case class MyResponse
(r: Result, id: String)

val task 
= tasks(id)
taskActor 
! MyRequest(task, {r => MyResponse(r, id)})

 

Now imagine a case where tasks are not the same. There are several types of tasks, each requiring a different type of handling on response. One way is for the cookie to contain the task type and branch accordingly when handling the response (“if (theType == …)…”). Another way is by having the ‘extract’ callback above create different response messages:

 

taskActor ! new MyRequest(task, {r => OneResponse(r, id)})
taskActor 
! new MyRequest(task, {r => AnotherResponse(r, id)})

react 
{
  case OneResponse
(r, id) => // handle tasks of type one
  case AnotherResponse
(r, id) => // handle tasks of another type
}

 

The problem here is that as task types grow, we need to add more response messages.  

An alternative is to encode the code handling the response inside the request construction:

 

case class MyResponse(r: Result, callback: Result => Unit)

taskActor 
! new MyRequest(task, {r => 
  MyResponse
(r, {r => 
    
// handle the result accordig to the type of task
  
}
})

react 
{
 case MyResponse
(r, callback) => callback(r)
}

 

This has several benefits:

  • The logic to request and handle the response is in one place in the code, making it easier to understand what is happening
  • The context at the scope of constructing the request is available to the callback. There’s no need to explicitly pass the id used before in the request and response, instead, the callback just uses it

 

Since much of the code above is boilerplate, it can be sugar coated :

 

taskActor ! request(task) {r =>
  
// handle the result
}
 


 

Thank you for your interest!

We will contact you as soon as possible.

Send us a message

Oops, something went wrong
Please try again or contact us by email at info@tikalk.com