Skip to content

Latest commit

 

History

History
 
 

lesson4

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 

Lesson 3.4: How to perform work asynchronously inside your actors using PipeTo

One of the first questions developers ask once they learn how Akka.NET actors work is…

"If actors can only process one message at a time, can I still use async methods or Task<T> objects inside my actors?"

Yes! You can still use asynchronous methods and Task<T> objects inside your actors - using the PipeTo pattern (instead of using await)!

This lesson will show you how.

Key Concepts / Background

"But wait!", you say. "Aren't actors already asynchronous?"

Indeed they are, and you make an astute point! Due to the nature of passing immutable messages between actors, actors are inherently thread-safe and asynchronous (they don't block each other).

But what if you want to do some asynchronous work from within an actor itself, such as kick off a long-running HTTP request via a Task?

Most developers would default to using await, which has achieved demigod status since its release in 2012.

And they would be making the wrong choice.

Why? To answer that, we need to review how actors process messages.

Actors process messages one at a time

Actors process the contents of their mailbox one message at a time. It looks like this:

Animation - Akka.NET actors processing messages in their mailbox

Why is maintaining this behavior critical?

Recall that immutable messages themselves are inherently thread-safe, since a different thread can't modify something that is immutable.

BUT: while the messages are inherently thread-safe, the message-processing code has no such guarantee!

Processing one message at a time is critical because making sure an actor's message processing code (OnReceive) can only be run one invocation at a time is how Akka.NET enforces thread-safety for all of the code that executes inside an actor.

An immutable message is pushed from the mailbox into OnReceive. Once the call to OnReceive exits, the actor's mailbox pushes a new message into the actor's OnReceive method.

That being said, it's still possible to take advantage of async methods and methods that return Task<T> objects inside the OnReceive method - you just have to use the PipeTo extension method!

Async message processing using PipeTo

The PipeTo pattern is a simple extension method built into Akka.NET that you can append to any Task<T> object.

public static Task PipeTo<T>(this Task<T> taskToPipe, ICanTell recipient, IActorRef sender = null)

Tasks are just another source of messages

The goal behind PipeTo is to treat every async operation just like any other method that can produce a message for an actor's mailbox.

THAT is the right way to think about actors and concurrent Task<T>s in Akka.NET. A Task<T> is not something you await on in Akka.NET. It's just something else that produces a message for an actor to process through its mailbox.

The PipeTo method takes an ICanTell object as a required argument, which tells the method where to pipe the results of an asynchronous Task<T>.

Here are all of the Akka.NET classes that you can use with ICanTell:

  • IActorRef - a reference to an actor instance.
  • ActorSelection - a selection of actors at a specified address. This is what gets returned whenever you look up an actor based on its path.

Most of the time, you're going to want to have your actors pipe the results of a task back to themselves. Here's an example of a real-world use case for PipeTo, drawn from our official Akka.NET PipeTo code sample.

// time to kick off the feed parsing process, and send the results to this same actor
Receive<BeginProcessFeed>(feed =>
{
    SendMessage(string.Format("Downloading {0} for RSS/ATOM processing...", feed.FeedUri));
    _feedFactory.CreateFeedAsync(feed.FeedUri).PipeTo(Self);
});

View the full source for this example..

Whenever you kick off a Task<T> and use PipeTo to deliver the results to some IActorRef or ActorSelection, here's how your actor is really processing its mailbox.

Animation - Akka.NET actors processing messages asynchronously in their mailbox using PipeTo

In this case we're using PipeTo to send the results back to itself, but you can just as easily send these results to different actor.

The important thing to notice in this animation is that the actor continues processing other messages while the asynchronous operation is happening.

That's why PipeTo is great for allowing your actors to parallelize long-running tasks, like HTTP requests.

Composing Task<T> instances using ContinueWith and PipeTo

Have some post-processing you need to do on a Task<T> before the result gets piped into an actor's mailbox? No problem - you can still use ContinueWith and all of the other TPL design patterns you used in procedural C# programming.

Here's another example from our PipeTo code sample:

// asynchronously download the image and pipe the results to ourself
_httpClient.GetAsync(imageUrl).ContinueWith(httpRequest =>
{
    var response = httpRequest.Result;

    // successful img download
    if (response.StatusCode == HttpStatusCode.OK)
    {
        var contentStream = response.Content.ReadAsStreamAsync();
        try
        {
            contentStream.Wait(TimeSpan.FromSeconds(1));
            return new ImageDownloadResult(image,
                response.StatusCode, contentStream.Result);
        }
        catch //timeout exceptions!
        {
            return new ImageDownloadResult(image, HttpStatusCode.PartialContent);
        }
    }

    return new ImageDownloadResult(image, response.StatusCode);
},
  TaskContinuationOptions.AttachedToParent &
  TaskContinuationOptions.ExecuteSynchronously)
 .PipeTo(Self);

View the full source for this example..

So in this case, we're downloading an image via a HttpClient inside an Akka.NET actor, and we want to check the status code of the HTTP response before we use PipeTo to deliver a message back to this actor.

So we do the HTTP code handling inside a ContinueWith block and use that to return an ImageDownloadResult message that will be piped to the actor using the PipeTo block. Pretty easy!

Why is await an Anti-pattern inside actors?

await is not magic, and breaks the core message processing guarantees

While await is a powerful and convenient construct, it isn't magic. It's just syntactic sugar for TPL continuation. If this is confusing or unfamiliar, we highly recommend reviewing Stephen Cleary's excellent Async/Await primer.

Await does two key things which break the core message processing guarantees of Akka.NET:

  1. Exits the containing async function, while
  2. Sets a continuation point in the containing method where the asynchronous Task (the awaitable) will return to and continue executing once it is done with its async work.

These actions by await have two negative effects:

First, await makes it harder to reason about exactly what is happening and on which thread. ContinueWith (which await is just syntactic sugar for anyway) makes it explicit and clear what is happening, and on which thread it's happening.

As we've discussed, an actor's mailbox pushes messages into the actor's OnReceive method as soon as the previous iteration of the OnReceive function exits. Whenever you await an async operation inside the OnReceive method, you prematurely exit the OnReceive method and the mailbox will push a new message into it, which is generally not what is intended.

Second, await breaks the "actors process one message at a time" guarantee. By the time the awaited Task returns and starts referencing things in the ActorContext, that context will have changed because the actor has moved on from the original message that awaited. Variables such as the Sender of the previous message may be different, or the actor might even be shutting down when the await call returns to the previous context.

So don't use await inside your actors. Await is evil inside an actor. Await is just syntactic sugar anyway. Use ContinueWith instead, and pair it with PipeTo.

This will turn the results of async operations into messages that get delivered to your actor's mailbox and you can take advantage of Task and other TPL methods just as you did before, and you'll enjoy nicely parallel processing!

Update: Akka.NET v1.0 now supports async / await inside ReceiveActor

Per the Akka.NET v1.0 release notes, native support for async and await is now available inside ReceiveActors.

public class MyActor : ReceiveActor
{
       public MyActor()
       {
             Receive<SomeMessage>(async some => {
                    //we can now safely use await inside this receive handler
                    await SomeAsyncIO(some.Data);
                    Sender.Tell(new EverythingIsAllOK());
             });
       }
}

There's some magic under the hood that takes care of this.

However, the PipeTo pattern is still the preferred way to perform async operations inside an actor, as it is more explicit and clearly states what is going on.

Do I need to worry about closing over (closures) my actor's internal state when using PipeTo?

Yes, you need to close over any state whose value might change between messages that you need to use inside your ContinueWith or PipeTo calls.

This usually means closing over the Sender property and any private state you've defined that is likely to change between messages.

For instance, the Sender property of your actor will definitely change between messages. You'll need to use a C# closure for this property in order to guarantee that any asynchronous methods that depend on this property get the right value.

Doing a closure is as simple as stuffing the property into an instance variable (var) and using that instance variable in your PipeTo call, instead of the field or property defined on your actor.

Here's an example of closing over the Sender property:

Receive<BeginProcessFeed>(feed =>
{
    // instance variable for closure
    // close over the current value of Sender, since it changes between
    // messages and accessing by property later would give different value
    var senderClosure = Sender;
    SendMessage(string.Format("Downloading {0} for RSS/ATOM processing...", feed.FeedUri));

    // send result of this async task back to the sender of the current message
    _feedFactory.CreateFeedAsync(feed.FeedUri).PipeTo(senderClosure);
});

NOTE: Assuming you're piping the result of the Task back to the same actor, you don't need to close over Self or Parent. Those IActorRefs will be the same when the Task returns. You just need to close over the state that is going to change by the time the Task completes and executes its continuation delegate.

Now, let's get to work and use this powerful parallelism technique inside our actors!

Exercise

Currently our GithubWorkerActor instances all block when they're waiting for responses back from the GitHub API, using the following code:

 var getStarrer = _gitHubClient.Activity.Starring.GetAllForUser(starrer);

// ewww
getStarrer.Wait();
var starredRepos = getStarrer.Result;
Sender.Tell(new StarredReposForUser(starrer, starredRepos));

We're going to leverage the full power of the TPL and allow each of our GithubWorkerActor instances kick off multiple parallel Octokit queries at once, and then use PipeTo to asynchronously deliver the completed results back to our GithubCoordinatorActor.

Take note - this the current speed of our GitHub scraper at the end of lesson 2:

GtihubActors at the end of lesson 2

Phase 1 - Replace GithubWorkerActor.InitialReceives

Open up Actors/GithubWorkerActor.csand replace the InitialRecieves method with the following code:

private void InitialReceives()
{
    // query an individual starrer
    Receive<RetryableQuery>(query => query.Query is QueryStarrer, query =>
    {
        // ReSharper disable once PossibleNullReferenceException
		// (we know from the previous IS statement that this is not null)
        var starrer = (query.Query as QueryStarrer).Login;

        // close over the Sender in an instance variable
        var sender = Sender;
        _gitHubClient.Activity.Starring.GetAllForUser(starrer).ContinueWith<object>(tr =>
        {
            // query faulted
            if (tr.IsFaulted || tr.IsCanceled)
                return query.NextTry();
            // query succeeded
            return new StarredReposForUser(starrer, tr.Result);
        }).PipeTo(sender);

    });

    // query all starrers for a repository
    Receive<RetryableQuery>(query => query.Query is QueryStarrers, query =>
    {
        // ReSharper disable once PossibleNullReferenceException
		// (we know from the previous IS statement that this is not null)
        var starrers = (query.Query as QueryStarrers).Key;


        // close over the Sender in an instance variable
        var sender = Sender;
        _gitHubClient.Activity.Starring.GetAllStargazers(starrers.Owner, starrers.Repo)
            .ContinueWith<object>(tr =>
            {
                // query faulted
                if (tr.IsFaulted || tr.IsCanceled)
                    return query.NextTry();
                return tr.Result.ToArray();
            }).PipeTo(sender);

    });
}

That's it!

Once you're done

Build and run GithubActors.sln - the performance should be really fast now.

GithubActors performance after lesson 4

At the start of the lesson, it took us 4 seconds to download our first 4 users for https://github.com/petabridge/akka-bootcamp. At the end of the lesson we downloaded 22 users in 4 seconds. All of this without adding any new actors or doing anything other than just letting the TPL work in concert via PipeTo.

NOTE: The GitHub API appears to be really slow for a handful of users on every repository we've tested. We have no idea why.

Great job!

Awesome - now you can use Task<T> instances in combination with your actors for maximum concurrency! Hooray!

Now it's time to move onto the final lesson: Lesson 5 - How to prevent deadlocks with ReceiveTimeout.

Further reading

See our full Akka.NET PipeTo sample.

Any questions?

Get Akka.NET training material & updates at https://www.getdrip.com/forms/3869566/submissions/new

Don't be afraid to ask questions :).

Come ask any questions you have, big or small, in this ongoing Bootcamp chat with the Petabridge & Akka.NET teams.

Problems with the code?

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.