Strategies for implementing request/response

Một phần của tài liệu Artima akka concurrency (Trang 445 - 453)

As we’ve already learned, the problem with the request/response idiom is establishing a context in which to understand the eventual response. For example, let’s say we have an actor that makes the plane go up and down. To any control amount that comes in, we want to tack on some sort of random

“deflection” amount, which an external actor calculates. Here’s the bad way to do it:

class BadIdea extends Actor { val deflection =

context.actorOf(Props[DeflectionCalculator]) def receive = {

case GoUp(amount) =>

deflection ! GetDeflectionAmount case GoDown(amount) =>

deflection ! GetDeflectionAmount case DeflectionAmount(amount) =>

// Hmm... was I going up or down?

} }

When we get the response from the deflection calculator, we don’t know whether we wanted to go up or down (i.e., we’ve lost ourcontext). We can solve this in a few different ways.

The future

Using a future lets us close over the data we might need and also provides a fairly natural contextual reference for our operation. We can take the

BadIdeaabove and convert it to aFutureIdealike this:

class FutureIdea(controlSurfaces: ActorRef) extends Actor { implicit val askTimeout = Timeout(1.second)

val deflection =

context.actorOf(Props[DeflectionCalculator]) def receive = {

case GoUp(amount) =>

// Ask for the deflection amount, transform it to a // StickBack message and pipe that to the Control // Surfaces

(deflection ? GetDeflectionAmount).collect { case amt: DeflectionAmount =>

val DeflectionAmount(deflection) = amt StickBack(amount + deflection)

} pipeTo controlSurfaces case GoDown(amount) =>

// Ask for the deflection amount, transform it to a // StickForward message and pipe that to the Control // Surfaces

(deflection ? GetDeflectionAmount).collect { case amt: DeflectionAmount =>

val DeflectionAmount(deflection) = amt StickForward(amount + deflection) } pipeTo controlSurfaces

} }

A really nice thing about the future is that we can put a timeout on the request. If the responder fails to respond in a timely manner, the future will throw an exception that we can deal with however we like. And it will throw that exception in a context-sensitive manner, so we can know that request

“B” failed due to a timeout, but request "D" succeeded just fine.

The actor

Sometimes a future won’t work out for you all that well. There may be many messages you want to handle, many states you want to transition, or just have complex logic that you want to employ. Even when they may be possible inside of a future, an actor may be a clearer representation of what you want to do. You can either cook up an actor to handle the problem for you, or you can do it anonymously. We’ll show the anonymous actor method here:

object ActorIdea {

import DeflectionCalculator._

import MockControls._

// Helper method to provide construction of StickBack // messages

def stickBack(amount: Float): Any = StickBack(amount) // Helper method to provide construction of StickForward // messages

def stickForward(amount: Float): Any = StickForward(amount) // Eliminates the need to write this code twice, for two // different message types.

//

// IMPORTANT: It also gets us outside of the main Actor's // context. Anything we close over in that main Actor will // ensure that we close over the main Actor itself. It may // or may not be a problem in particular circumstances, but // it's good practice to avoid it, regardless of the

// circumstances.

def anon(sendTo: ActorRef,

controlSurfaces: ActorRef, amount: Float,

respondWith: Float => Any): Props = Props(new Actor {

override def preStart() = sendTo ! GetDeflectionAmount def receive = {

case DeflectionAmount(deflection) =>

controlSurfaces ! respondWith(amount + deflection)

// Remember to stop yourself!

context.stop(self) }

}) }

class ActorIdea(controlSurfaces: ActorRef) extends Actor { import ActorIdea._

val deflection =

context.actorOf(Props[DeflectionCalculator]) def receive = {

case GoUp(amount) =>

// Spin up an anonymous Actor to send the StickBack // message to the Control Surfaces based on the // deflection amount

context.actorOf(anon(deflection, controlSurfaces, amount, stickBack))

case GoDown(amount) =>

// Spin up an anonymous Actor to send the StickForward // message to the Control Surfaces based on the

// deflection amount

context.actorOf(anon(deflection, controlSurfaces, amount, stickForward)) }

}

This is much like the future example, but it’s not so easy to put the time- out on the responses. You’d have to use the ReceiveTimeoutmessage to indicate that something failed to be received. If you have more than one thing happening here, though, that can get tricky as the receive timeout re- sets when it getsanymessage at all. Keep thingssimple.

Did you notice that we broke with good Akka convention and didn’t name the anonymous actors? Well, if you didn’t, have a look and think about why that may be the case. Don’t worry, I’ll wait. . .

OK, got it? What’s going to happen ifGoUp(5f)is sent toActorIdeain reallyquick succession? Yup; the code that handles that message—the one that spins up the anonymous actor to send theStickBackmessage—is going to be called twice. It’s also, probably, going to instantiate two anonymous

actors in the same actor system with the same name. That’s going to be a big problem, and you’re not going to see it until things get heavily loaded.

Tip

Watch out when you’re naming an anonymous actor that’s designed to be short lived. Akka defaults to providing a name for you and, while that name isn’t necessarily descriptive, it’s a cheap way to provide uniqueness.

If you keep your actors simple, then this is going to be perfectly reasonable most of the time so don’t worry about it. It’s much better to have a name pop up like$abthen it is to get an exception regarding lack of uniqueness in a name.

Internal actor data

We can use mutable data inside the actor to help the operational context survive between message processing.

class VarIdea(controlSurfaces: ActorRef) extends Actor { val deflection =

context.actorOf(Props[DeflectionCalculator]) var lastRequestWas = ""

var lastAmount = 0f def receive = {

case GoUp(amount) =>

lastRequestWas = "GoUp"

lastAmount = amount

deflection ! GetDeflectionAmount case GoDown(amount) =>

lastRequestWas = "GoDown"

lastAmount = amount

deflection ! GetDeflectionAmount case DeflectionAmount(deflection) if lastRequestWas == "GoUp" =>

controlSurfaces ! StickBack(deflection + lastAmount) case DeflectionAmount(deflection)

if lastRequestWas == "GoDown" =>

controlSurfaces ! StickForward(deflection + lastAmount) }

}

This is pretty hideous, in the general sense, but in certain specific cases it may serve you well. There are several pitfalls to it:

1. It relies on getting requests and responses in a particular or- der; i.e., GoUp, DeflectionAmount, GoDown, DeflectionAmount,

GoDown,DeflectionAmount,etc. If you getGoDown,GoDown,GoUp,

DeflectionAmount, then it won’t work well at all.

2. It doesn’t survive a restart very well. When it gets constructed, the internal data goes back to their initialized states.

3. It’s not very resilient to change. If your actor becomes more complex, this varying state of the data may become entirely unwieldy. The state of the request is not localized to the request, but is now global to the actor. It really can only be doing one thing, as opposed to processing several different things between requests and their paired responses.

4. You have to deal with timeouts. If the responder doesn’t respond by the time you’d like, you need to figure out how to deal with that.

Message data

You can also use the message to store this data, which gives you the low overhead of machine usage (don’t have to spin up a future or actor), at the price of muddying up the protocol a bit.

class MsgIdea(controlSurfaces: ActorRef) extends Actor { val deflection =

context.actorOf(Props[DeflectionCalculator2]) def receive = {

case GoUp(amount) =>

deflection ! GetDeflectionAmount("GoUp", amount) case GoDown(amount) =>

deflection ! GetDeflectionAmount("GoDown", amount) case DeflectionAmount(op, amount, deflection)

if op == "GoUp" =>

controlSurfaces ! StickBack(deflection + amount) case DeflectionAmount(op, amount, deflection) if op == "GoDown" =>

controlSurfaces ! StickForward(deflection + amount) }

}

The price paid here is that theDeflectionCalculator2must package up the context data in its responses. There are pitfalls here as well:

1. It opens the door to runtime errors should the data’s repackaging get screwed up somehow.

2. It increases the coupling between requester and responder.

3. It decreases code flexibility as you wish to add or remove fields.

• This can be mitigated by providing a single case class member that holds all of the fields, so that the responder remains some- what ignorant of the issue.

4. It increases payloads that have to go across the network should you want to send these messages to remote nodes.

5. You have to deal with timeouts. If the responder doesn’t respond by the time you’d like, you need to figure out how to deal with that.

Internal/message data hybrid

You can hit a middle ground between the internal data and the message data approaches that solves some of the issues. You put a minimal context into the message, which we call atag,and then use that tag as an index into some internal data to the actor.

class TagIdea(controlSurfaces: ActorRef) extends Actor { import scala.collection.mutable.Map

val deflection =

context.actorOf(Props[DeflectionCalculator3]) // The map of tags to context

val tagMap = Map.empty[Int, Tuple2[String, Float]]

// Our 'tags' will be integers var tagNum = 1

def receive = {

case GoUp(amount) =>

// Add the req / rsp context to the map tagMap += (tagNum -> ("GoUp", amount)) deflection ! GetDeflectionAmount(tagNum) tagNum += 1

case GoDown(amount) =>

// Add the req / rsp context to the map tagMap += (tagNum -> ("GoDown", amount)) deflection ! GetDeflectionAmount(tagNum) tagNum += 1

case DeflectionAmount(tag, deflection) =>

// Get the req / rsp context from the map val (op, amount) = tagMap(tag)

val amt = amount + deflection // Remove the context from the map tagMap -= tag

if (op == "GoUp") controlSurfaces ! StickBack(amt) else controlSurfaces ! StickForward(amt)

} }

We can now have multiple things going on at once, which is great, but of course there are still pitfalls here:

1. The message protocol is still a bit messy. What if they send back the wrong tag? Yeah, that could be serious death.

2. We still have response timeouts to worry about. You might have to have something that stores the request timestamp in the map so that you can sweep through it occasionally to see if anything has timed out.

3. You’d have to decide how you behave if you restart.

Roundup

OK? There are several different ways to handle the request/response issue and there are really two major things that seem to sway the decisions on how you might want to do it:

1. Ease of implementation. Future wins here. . . big time. You can easily close over what you’d like (well, excluding those things youshouldn’t close over, of course), naturally create the response context you need, and everything is hunky dory.

2. Speed. Spinning up a future to do the work for you may be costly so you might need to avoid it. That said,don’t prematurely optimize.

If you’re trying to do millions of request/responses per second, that may or may not be costly. If a profiler tells you that it is, and your users are experiencing a latency that you can blame on the future request/response, then you might consider changing it for those rare cases where you need to. At that point, you need to pick one of the other methods that works in your situation.

Một phần của tài liệu Artima akka concurrency (Trang 445 - 453)

Tải bản đầy đủ (PDF)

(515 trang)