Writing applications with Scala actors in practice

2020-06-05 17:19发布

问题:

Because my first question was so long, I'm asking this as a separate question. It's another one about the architecture of an actor-based application.

Keeping track of message paths through an Application

Let's take a piece of Java code:

public void deleteTrades(User user, Date date) {
    PermissionSet ps = permissionService.findPermissions(user)
    if (ps.hasPermission("delete")) {
        Set<Trade> ts = peristence.findTrades(date);
        reportService.sendCancelReports(ts);
        positionService.updateWithDeletedTrades(ts);
    }
}

In this code I have 4 separate components and the interaction between them required for the procedure deleteTrades is well-defined. It's completely contained in the method deleteTrades.

Modelling this with Actors and replacing my 4 components with 4 separate actors, how do I keep track (in my mind) of what a procedure involves? Particularly if I'm avoiding using the !? operator, then it's likely that I'll be sending a message ConditionalDelete to my PermissionActor, which will be sending a message GetTradesAndDelete to my PersistenceActor which will then send further messages etc etc. The code to process a delete will be strewn across my application.

It also means that pretty much every actor needs a handle on every other actor (in order to forward messages).

As in my previous question, how do people deal with this? Is there a good modelling tool which lets you keep track of all this? Do people use !? Am I turning too many components into Actors?

回答1:

You use 5 components, definitely. There are actors dealing with specific tasks, and there's an orchestrator as well.

The question you must have, of course, is how do you chain this assynchronously. Well, it's actually somewhat easy, but it can obscure the code. Basically, you send each componenent the reply you want.

react {
  case DeleteTrades(user,dates) => 
    PermissionService ! FindPermissions(user, DeleteTradesPermissions(dates) _)
  case DeleteTradesPermissions(dates)(ps) =>
    if (ps hasPermission "delete")
      Persistence ! FindTrades(date, DeleteTradesTradeSet _)
  case DeleteTradesTradeSet(ts) =>
    ReportService ! SendCancelReports(ts)
    PositionService ! UpdateWithDeletedTrades(ts)
}

Here we use currying to pass "dates" in the first returning answer. If there's a lot of parameters associated with an interaction, it might be better to keep the information for all on-going transactions in a local HashSet, and just pass a token that you'll use to locate that information when receiving the answer.

Note that this single actor can handle multiple concurrent actions. In this particular case, just Delete transactions, but you could add any number of different actions for it to handle. When the data needed for one action is ready, then that action continues.

EDIT

Here's a working example of how these classes can be defined:

class Date
class User
class PermissionSet

abstract class Message
case class DeleteTradesPermission(date: Date)(ps: PermissionSet) extends Message
case class FindPermissions(u: User, r: (PermissionSet) => Message) extends Message

FindPermissions(new User, DeleteTradesPermission(new Date) _)

A few explanations on currying and functions. The class DeleteTradesPermission is curried so that you can pass a Date on it, and have some other function complete it with a PermissionSet. This would be the pattern of the answer messages.

Now, the class FindPermissions receives as a second parameter a function. The actor receiving this message will pass the return value to this function, and will receive a Message to be sent as answer. In this example, the message will have both the Date, which the calling actor sent, and PermissionSet, which the answering actor is providing.

If no answer is expected, such as the case of DeleteTrades, SendCancelReports and UpdateWithDeletedTrades for the purposes of this example, then you don't need to pass a function of the returning message.

Since we are expecting a function which returns a Message as parameter for those messages requiring an answer, we could define traits like this:

trait MessageResponse1[-T1] extends Function1[T1, Message]
trait MessageResponse2[-T1, -T2] extends Function2[T1, T2, Message]
...


回答2:

Actors should not be used to replace traditional service components without considerations.

Most of the service components we write nowadays, by training, are stateless. Stateless service components are easier to manage (sans message class, etc) than actors. One of the things they lack though, when compare to actors, is asynchronous execution. But when clients are expecting the results to return synchronously most of the time, synchronous stateless service components are just fine for the job.

Actor is a good fit when there are internal states to manage. There is no need to do any synchronization inside "act()" to access internal states and to worry about race conditions. As long as "!?" is not used inside "act()", deadlocking should be minimized as well.

Actors should be wary of any blocking processing done while handling messages. Since actors process their messages sequentially, any time they are blocked waiting for I/O inside "act()", they can not process any other messages in their mailboxes. The Scala idiom to use here is to start another ad-hoc actor that does the actual blocking operation. This anti-pattern affects event-based(react) actor even more because it blocks the thread the event-based actor is piggy-backed on as well.

From what I can gather, all four of your calls to the service components are potentially blocking, so cares should be taken to make them scale when converting them to actors.