C++ Actor Framework — Dev Blog

An Open Source implementation of the actor model in C++

First Pre-release of Version 0.15 Released!

The first pre-release version of 0.15 has just been released! As outlined in our previous post, this version comes with breaking changes. However, we think the end result is well worth it. This release is the result of 8 months of development and 374 commits.

The best starting points to learn the new API is to read the examples and the extensively updated manual (as HTML or PDF). Special thanks go to our early adopters for feedback, discussion, and bug fixing!

Our goal with this pre-release is to stabilize the API. We have one planned feature for the next pre-release that allows users to write less boilerplate code for serialization and string conversion. However, this change will be backwards compatible. Please do not hesitate to propose changes to the current API on GitHub.

By the way, configuring your CAF application is now incredibly easy. We support CLI options as well as separate INI files. The new manual has an entire section dedicated to the new extensible config mechanism.

We plan to release the next pre-release end of July. Whether or not we release a third pre-release version depends on the amount of user feedback we get (and how many bugs we find). If all goes well, we will ship 0.15 in August.

A Glimpse at the Future of CAF

Sometimes, a design outlives its usefulness. CAF started with its first commit on March 4, 2011. Its mission statement? Provide a lightweight, domain-specifc language (DSL) for actors in C++. The user-facing API should be as minimalistic as possible by hiding internal state of the actor runtime. Key components, such as scheduler or middleman, run as lazily initialized singletons. This design works great as long as applications do not wish to configure parameters of these global components. With a growing user base and more fields of applications, it becomes clear that hiding as much state of the system as possible puts obstacles in the way of many users.

After much consideration, we decided to start over with a API in 0.15. The next release of CAF embodies our vision for a 1.0 release, and addresses the vibrant feedback we received over the years. The new anchor of CAF applications is actor_system, which replaces the previous singleton-based design. An actor system encapsulates runtime state, which CAF previously maintained globally, such as announced type information and scheduler behavior. Users can now fully control the initialization phase of an actor system. Aside from improved configurability, this change has the advantage that multiple actor systems can now co-exist in the same process, e.g., one with a scheduler tuned for high-throughput and one with a scheduler optimized for low latency scenarios.

With CAF 0.15, we replaced misleading function names with more intuitive ones (e.g., sync_send with request). We also improved the reference counting implementation used for actor garbage collection. Without further ado, this is how the Hello World example will look in 0.15:

behavior mirror(event_based_actor* self) {
  // return the (initial) actor behavior
  return {
    // a handler for messages containing a single string
    // that replies with a string
    [=](const string& what) -> string {
      // prints "Hello World!" via aout (thread-safe cout wrapper)
      aout(self) << what << endl;
      // reply "!dlroW olleH"
      return string(what.rbegin(), what.rend());
    }
  };
}

void hello_world(event_based_actor* self, const actor& buddy) {
  // send "Hello World!" to our buddy ...
  self->request(buddy, std::chrono::seconds(10), "Hello World!").then(
    // ... wait up to 10s for a response ...
    [=](const string& what) {
      // ... and print it
      aout(self) << what << endl;
    }
  );
}

int main() {
  // our CAF environment
  actor_system system;
  // create a new actor that calls 'mirror()'
  auto mirror_actor = system.spawn(mirror);
  // create another actor that calls 'hello_world(mirror_actor)';
  system.spawn(hello_world, mirror_actor);
  // system will wait until both actors are destroyed before leaving main
}

For comparison, this is the same code in 0.14:

behavior mirror(event_based_actor* self) {
  // return the (initial) actor behavior
  return {
    // a handler for messages containing a single string
    // that replies with a string
    [=](const string& what) -> string {
      // prints "Hello World!" via aout (thread-safe cout wrapper)
      aout(self) << what << endl;
      // terminates this actor ('become' otherwise loops forever)
      self->quit();
      // reply "!dlroW olleH"
      return string(what.rbegin(), what.rend());
    }
  };
}

void hello_world(event_based_actor* self, const actor& buddy) {
  // send "Hello World!" to our buddy ...
  self->sync_send(buddy, "Hello World!").then(
    // ... wait for a response ...
    [=](const string& what) {
      // ... and print it
      aout(self) << what << endl;
    }
  );
}

int main() {
  // create a new actor that calls 'mirror()'
  auto mirror_actor = spawn(mirror);
  // create another actor that calls 'hello_world(mirror_actor)';
  spawn(hello_world, mirror_actor);
  // wait until all other actors we have spawned are done
  await_all_actors_done();
  // run cleanup code before exiting main
  shutdown();
}

Most notably, we no longer need to call await_all_actors_done() and shutdown() in main(). Nor does the mirror actor need to call quit() explicitly. Once system goes out of scope, it will wait (i.e., block) for all remaining actors before cleaning up. The mirror actor is destroyed implicitly once there is no more reference to it. Finally, sync_send has been renamed to request and now requires a timeout.

Additionally, dynamically and statically typed actors now exhibit a symmetric API. We also provide new ways to compose actors and the behavior of actors. If you are interested in the current state of development, check out the topic/actor-system branch. It is in development since November 2015, currently contains 200 commits, and will get merged into develop soon.

Since version 0.15 has many API changes, we will release at least one pre-release version for collecting user feedback and finalizing the API. The first pre-release is scheduled for June and will include a complete overhaul of the user manual. It was not an easy decision to break the API on so many layers. However, the changes are the result of all the feedback we received over years as well as our own experiences with using CAF on a daily basis. This makes version 0.15 the most important milestone towards a stable 1.0 release.

Spotlight: Bro

For our second article in the spotlight series, we want to higlight the popular open source network monitor Bro and talk to the core development team in Berkeley, California.

Background: Bro(ker)

Official Bro Logo


Bro is an open source network analysis framework, well grounded in 15 years of research. While focusing on network security monitoring, Bro provides a comprehensive platform for more general network traffic analysis as well. Bro's user community includes major universities, research labs, supercomputing centers, open-science communities, and has an established foothold in the industry. In addition to organize user conferences and providing online resources for learning and using Bro, the creators of Bro also offer enterprise-level commercial support.

With version 2.4, the Bro development team integrated a new communication library based on CAF called Broker (not to be confused with brokers in CAF). Currently, this new communication layer is shipped as beta release and needs to be enabled explicitly during build. Broker will become a mandatory dependency in future Bro versions and replace the current communication and serialization system.

In this interview, we have the pleasure to talk to the core development and research team primarily located at the International Computer Science Institute (ICSI) in Berkeley, California.

The Interview

CAF Team: Dear Bro team, thank you for giving us the opportunity for this interview. The original paper describing Bro was released in 1999. This is a very long time for a software code base and clearly speaks for its quality. What is your secret?

Bro Team: When Vern Paxson created the initial version Bro, he designed the system out of practical experience and---as we can say in hindsight---came up with the right abstractions in the system architecture, which at its core hasn't changed since.

We also carefully try to avoid feature creep and only add new functionality when finding convincing use cases, which often involves discussion. While this sometimes comes at the of cost development speed, it often helps us to make strategically viable decisions in the long term.

CAF Team: Bro is a security-critical software and network operators around the world rely on it. Changing a key component like the communication system is not something that you do without having a good reason. What was your motivation to start developing a new communication backend from scratch?

Bro Team: To keep monitoring the ever-growing network uplinks at line rate today (10, 40, or even 100 Gbps), the traffic needs to be distributed across several physical systems. Logs are sent back to a single system, where they need to be quickly and reliably written out. In addition, certain types of analysis are distributed across all of the nodes (e.g. scan detection) and stress the communications framework.

Our existing communication framework starts to show its age: there exist race conditions with synchronization of high-level state, it lacks a persistence component, the pub/sub data flow cannot be controlled in a fine-grained way, the implementation is complex and inefficient, and we have to maintain two independent implementations: there exists a separate C library (Broccoli) to allow external applications to interact with Bro.

CAF Team: What goals did you set for the new communication system? What do you want to achieve with the system in the long run?

Bro Team: Ultimately we want to "crack open" the boundaries of what constitutes Bro and who can communicate with it. We also want to consolidate our existing implementation into a single library, which both Bro and external applications can use. In the past, Bro spawned a separate child process for its communication. Now we're just linking against a shared library---Broker---which provides a uniform API to communication in the Bro ecosystem.

We want to create a platform where it's easy to communicate with Bro instances, reconfigure them on the fly, and change their state not just from within Bro, but from anywhere. Moreover, we aim to provide a more flexible pub/sub event communication infrastructure.

CAF Team: Many challenges in software development arise in the implementation step and are not obvious in the initial design phase. What were the core issues and challenges you faced when implementing Broker and how did CAF contribute to your solution?

Bro Team: From a high level, ripping out a communication infrastructure and replacing it with a new one, without users even noticing, is a big challenge. This means we have to be able to incrementally deploy Broker. We already shipped Broker with Bro 2.4, but it's not yet enabled by default. However, our master branch already relies on Broker, which means that 2.5 will come with Broker enabled by default after we get enough feedback from our users and putting out release candidates. The transition to C++11 so far caused most of the attention.

From the implementation side, we wanted to converge to a tight, minimal API that proves powerful enough to cover our use cases. CAF gives us the flexibility we need to hide a powerful asynchronous backend behind a blocking C API, but also expose the asynchrony at at Broker's C++ API. Then there's the messaging aspect: Broker is essentially a distributed pub/sub engine which naturally maps to the abstractions CAF provides.

One of our biggest challenges involves bringing together asynchronous lookups and type-erasure in the Bro's statically typed scripting language. For example, when asking a remote data store for a value, the return type is not known a priori. Fortunately, we already have proposal for adding pattern matching (note to the security community: not regular expressions) for type-safe message access.

CAF Team: Did you consider other frameworks? If yes, what convinced you to use CAF instead?

Bro Team: We looked at other, more low-level messaging libraries. In particular, we compared CAF to nanomsg. With CAF we found that we could express the same idea in significantly less code because it comes with everything we need of the box. CAF simply doesn't require any boilerplate and enabled us to focus on the problem domain, as opposed to grappling with the implementation details of messaging framework. After all, avoiding complexity and being able to reason about both code and protocol was the primary motivator for rewriting our communication layer.

The type-safe abstractions CAF provides on top of bare messaging made a huge difference in productivity. One of our team members summarized it as "[..] it didn't feel like I was writing code that attempts to implement a protocol as it felt like writing code that is the protocol."

We also found that nanomsg's communication patterns, which represent one of its key selling points, can be easily implemented in a few lines of code with CAF. Orthogonal to that, CAF comes with a nice failure semantics (monitors & links) which we haven't found in other frameworks.

Finally, if you didn't release CAF under a BSD (or equivalently permissive) license, we couldn't even have considered it in the first place.

CAF Team: How were the reactions to CAF in your team so far?

Bro Team: Internally we had one CAF fan already who brought the actor model to the attention of the team. After our comparative evaluation, we were all excited to see such a clear winner and looking forward to seeing CAF perform in more than just toy examples.

CAF Team: Can you tell us about your performance and scaling requirements? Did CAF meet your expectations?

Bro Team: Broker enables us to reach a new horizon in terms of scaling. Bro is one of the unique open system capable of monitoring 100 Gbps---a workload which only a cluster of machines can stem. The worker nodes must send the logs somewhere via Broker, while the messaging layer must not induce a high overhead so that the nodes can focus on their traffic analysis.

In addition to the standard cluster for monitoring a single link, we have an ongoing "deep cluster" project where multiple sensor nodes deep in the network augment the traditional boarder-gateway monitoring. This entails deploying hundreds and thousands of nodes at global scale.

Finally, Bro can instruct open-flow controllers to reconfigure network hardware, e.g., to insert blocking rules or shunt flows. These use cases have tight latency requirements, with Broker must meet.

We are still in the process of evaluating performance; things look good so far but we expect to do more tuning as our users start to rely on it for operations.

CAF Team: The official announcement of Broker was at BroCon 2015. How did your community react to your plans for the communication system? Did they see the benefits immediately or did they object to changing critical components and introducing CAF as a new long-term dependency?

Bro Team: Our users liked that Broker enables new ways of communicating with Bro in the future. We're already receiving first questions about Broker on the mailing list.

Fortunately, our community trusts us developers that we make the right strategic decisions about Bro's future. So we haven't heard any critical voices about introducing CAF as new dependency. In fact, moving to C++11 will allows us to modernize Bro's code base itself.

CAF Team: Final question: if you were to decide what the next feature of CAF would be, what would you have in mind?

Bro Team: In the near term, we want to migrate to typed actors so that we can get a compile-time proof that our messaging protocol implements the spec correctly. But CAF can do that already. In the short term, we want to use the direct connection optimization for when distant siblings in a tree hierarchy communicate. Further down the road, we're excited about the planned introspective features, where CAF can report utilization of CPU I/O, which we would like to use a basis for decisions to adapt our topology dynamically at runtime. CAF's stateful and migratable actors seem to lay the foundation for this capability.

CAF Team: Thank you very much for this interview and we wish you all the great success for the next 15 years of Bro!

Special thanks go to Matthias Vallentin for introducing the teams to each other. Matthias is a member of the Bro team as well as a long-time contributor to CAF.

Links