Awesome, look at you go! By now, you understand the massive increases in throughput that routers can give you, and what the different types of routers are.
Now we need to show you how to configure and deploy them.
We first learned about HOCON in Lesson 2.1.
To review, HOCON (Human-Optimized Config Object Notation) is a flexible and extensible configuration format. It will allow you to configure everything from Akka.NET's IActorRefProvider
implementation, logging, network transports, and more commonly - how individual actors are deployed.
It's this last feature that we'll be using here to configure how our router actors are deployed. An actor is "deployed" when it is instantiated and put into service within the ActorSystem
somewhere.
There are three key reasons that we prefer using HOCON to configure our routers.
First, using HOCON keeps your code cleaner. By using HOCON you can keep configuration details out of your application code and keep a nice separation of concerns there. Helps readability a lot, too.
Second, like any actor, a router can be remotely deployed into another process. So if you want to remotely deploy a router (which you will), using HOCON makes that easier.
But most importantly, using HOCON means that you can change the behavior of actors dramatically without having to actually touch the actor code itself, just by changing config settings.
What specific flags you need to specify will depend on the type of router you're using (e.g. you will need a duration
with a ScatterGatherFirstCompletedRouter
), but here are the things you'll be configuring the most.
The most common thing you'll specify is what the type of router is.
Here are the mappings between ´deployment.router' short names to fully qualified class names. You'll use these short names in App.config
:
router.type-mapping {
from-code = "Akka.Routing.NoRouter"
round-robin-pool = "Akka.Routing.RoundRobinPool"
round-robin-group = "Akka.Routing.RoundRobinGroup"
random-pool = "Akka.Routing.RandomPool"
random-group = "Akka.Routing.RandomGroup"
balancing-pool = "Akka.Routing.BalancingPool"
smallest-mailbox-pool = "Akka.Routing.SmallestMailboxPool"
broadcast-pool = "Akka.Routing.BroadcastPool"
broadcast-group = "Akka.Routing.BroadcastGroup"
scatter-gather-pool = "Akka.Routing.ScatterGatherFirstCompletedPool"
scatter-gather-group = "Akka.Routing.ScatterGatherFirstCompletedGroup"
consistent-hashing-pool = "Akka.Routing.ConsistentHashingPool"
consistent-hashing-group = "Akka.Routing.ConsistentHashingGroup"
}
The second most common flag you'll specify in HOCON is the number of routee instances to place under the router.
You do this with the nr-of-instances
flag, like so:
akka {
actor{
deployment{
/myRouter{
router = broadcast-pool
nr-of-instances = 3
}
}
}
}
To use a ResizablePoolRouter
("auto scaling router"), a Resizer
component is required. This is the component that does the monitoring of routee mailbox load and compares that to the thresholds it has calculated.
Out of the box, there is only the default Resizer
. You can configure your own if you want, but be forewarned, it's complicated. Which Resizer
to use is commonly specified in HOCON, like so:
akka.actor.deployment {
/router1 {
router = round-robin-pool
resizer {
enabled = on
lower-bound = 2
upper-bound = 3
}
}
}
The only thing we can think of that MUST be configured procedurally is the HashMap
function given to a ConsistentHashRouter
.
Everything else we can think of can be configured either way, but we prefer to do all our configuration via HOCON.
HOCON wins. This is true for all actors, not just routers.
For example, if you procedurally specify config for a router and also configure the router in App.config
, then the values specified in HOCON win.
Suppose you defined the following router via configuration:
/router1 {
router = consistent-hashing-pool
nr-of-instances = 3
virtual-nodes-factor = 17
}
But when it came time to create router1, you gave Props
the following procedural router definition:
var router1 = MyActorSystem.ActorOf(Props.Create(() =>
new FooActor()).WithRouter(new RoundRobinPool(10)), "router1");
You'd still get a ConsistentHashingPool
with 3 instances of FooActor
instead of a RoundRobinPool
with 10 instances of FooActor
.
Akka.NET won't create an actor with a router unless you explicitly call WithRouter
during the time you create an actor who needs to be a router.
So, if we want to rely on the router definition we've supplied via configuration, we can use the FromConfig
class to tell Akka.NET "hey, look inside our configuration for a router specification for this actor."
Here's an example:
/router1 {
router = consistent-hashing-pool
nr-of-instances = 3
virtual-nodes-factor = 17
}
And if we make the following call to ActorOf
:
var router1 = MyActorSystem.ActorOf(Props.Create(() =>
new FooActor()).WithRouter(FromConfig.Instance), "router1");
Then we'll get a ConsistentHashingPool
router.
Otherwise, if we just called this:
var router1 = MyActorSystem.ActorOf(Props.Create(() => new FooActor()), "router1");
Then we'd only get a single instance of FooActor
in return. Use FromConfig
whenever you need to use a router defined in configuration.
Bonus concept! We're also going to teach you to use Ask
in addition to HOCON.
Ask
is how one actor can ask another actor for some information and wait for a reply.
Whenever you want one actor to retrieve information from another and wait for a response. It isn't used that often—certainly not compared to Tell()
—but there are places where it is exactly what you need.
Great! Let's put Ask
and HOCON to work with our routers!
We're not going to change the actor hierarchy much in this lesson, but we are going to replace the programmaticly defined BroadcastGroup
router we created in lesson 1 with a BroadcastPool
defined via HOCON configuration in App.config
.
We'll add our configuration section first, before we modify the code inside GithubCommanderActor
.
Open App.config
and add the following inside the akka.actor.deployment
section:
<!-- inside App.config, in the akka.actor.deployment
section with all of the other HOCON -->
<!-- you can add this immediately after the
/authenticator deployment specification -->
/commander/coordinator{
router = broadcast-pool
nr-of-instances = 3
}
Open up Actors/GithubCommanderActor.cs
and do the following:
Add the following import to the top of the file:
// add to the top of Actors/GithubCommanderActor.cs
using System.Linq;
Replace the GithubCommanderActor
's `BecomeAsking method to look like this:
// GithubCommanderActor's BecomeAsking method - replace it with this
private void BecomeAsking()
{
_canAcceptJobSender = Sender;
// block, but ask the router for the number of routees. Avoids magic numbers.
pendingJobReplies = _coordinator.Ask<Routees>(new GetRoutees())
.Result.Members.Count();
Become(Asking);
}
Since the number of routees underneath _coordinator
is now defined via configuration, we're going to Ask<T>
the router using a built-in GetRoutees
message to determine how many replies we need (1 per routee) before we can accept a new job. This is a special message that tells the router to return the full list of all of its current Routees
back to the sender.
Ask
is usually an asynchronous operation, but in this case we're going to block and wait for the result - because the GithubCommander
can't execute its next behavior until it knows how many parallel jobs can be run at once, which is determined by the number of routees.
NOTE: Blocking is not evil. In the wake of
async
/await
, many .NET developers have come to the conclusion that blocking is an anti-pattern or generally evil. This is ludicrous. It depends entirely on the context. Blocking is absolutely the right thing to do if your application can't proceed until the operation you're waiting on finishes, and that's the case here.
Finally, replace the GithubCommanderActor
's PreStart
method with the following:
// replace GithubCommanderActor's PreStart method with this
protected override void PreStart()
{
// create a broadcast router who will ask all
// of them if they're available for work
_coordinator =
Context.ActorOf(Props.Create(() => new GithubCoordinatorActor())
.WithRouter(FromConfig.Instance),
ActorPaths.GithubCoordinatorActor.Name);
base.PreStart();
}
And that's it!
Build and run GithubActors.sln
- you'll notice now that everything runs the same as it was before, but if you modify the nr-of-instances
value in the deployment configuration for /commander/coordinator
then it will directly control the number of parallel jobs you can run at once.
Effectively you've just made the number of concurrent jobs GithubActors.sln
can run at once a configuration detail - cool!
We've been able to leverage routers for parallelism both via explicit programmatic deployments and via configuration.
And now it's time to achieve maximum parallelism using the TPL in the next lesson: Lesson 4 - How to perform work asynchronously inside your actors using PipeTo
Come ask any questions you have, big or small, in this ongoing Bootcamp chat with the Petabridge & Akka.NET teams.
If there is a problem with the code running, or something else that needs to be fixed in this lesson, please create an issue and we'll get right on it. This will benefit everyone going through Bootcamp.