Introduction and Practice of rotor/0.36: A Powerful C++ Actor Microframework

rotor/0.36: Introduction and Practice of a C++ Actor Microframework

1. Introduction to rotor

rotor is an event loop-friendly C++ Actor microframework designed for building efficient asynchronous messaging systems. The framework is inspired by The Reactive Manifesto and SObjectizer, aiming to provide a simple yet powerful implementation of the Actor model.

1.1 Core Features

rotor has the following notable features:

  • Multiple event loop support: Seamlessly integrates with various event loops such as wx, boost-asio, ev, etc.
  • Erlang-style supervision mechanism: Provides a hierarchical supervision tree to ensure system reliability.
  • High-performance messaging: Supports asynchronous request-response and publish-subscribe patterns.
  • Cross-platform support: Compatible with multiple operating systems including Windows, macOS, and Linux.
  • Lightweight design: The core library is simple and resource-efficient, suitable for embedded and high-performance computing environments.

1.2 Application Scenarios

rotor is particularly suitable for the following scenarios:

  • Network servers: High-performance messaging mechanisms and event loop support.
  • Distributed systems: Ensures automatic recovery of components in case of failure through the supervision tree.
  • Embedded systems: Lightweight implementation with low resource consumption.
  • Real-time systems: Low-latency messaging, suitable for real-time data processing.

2. Environment Configuration and Installation

2.1 Environment Preparation

Before using rotor, ensure that the following tools and libraries are installed on your system:

  • CMake (version 3.15 or higher)
  • C++ compiler (GCC or Clang)
  • Boost library (version 1.83.0 or higher)

2.2 Getting rotor

Clone the rotor project to your local machine:

git clone https://github.com/basiliscos/cpp-rotor.git
cd cpp-rotor

2.3 Building the Project

Use CMake to build the project:

mkdir build
cd build
cmake ..
make

3. Core Concepts and Basic Usage

3.1 Basics of Actor

In rotor, an Actor is the basic unit of concurrent computation, with its own state and behavior, communicating with other Actors through message passing.

Here is a simple example of creating an Actor:

#include <rotor/rotor.h>
#include <iostream>

namespace r = rotor;

// Define a simple ping Actor
class ping_actor : public r::actor_base_t {
public:
    using r::actor_base_t::actor_base_t;

    void set_pong_addr(const r::address_ptr_t &addr) {
        pong_addr = addr;
    }

    void on_start() noexcept override {
        r::actor_base_t::on_start();
        // Send the first message after starting
        send<ping_message>(pong_addr);
    }

    void on_pong_message(pong_message &msg) {
        std::cout << "ping: received pong" << std::endl;
        // Reply with ping message
        send<ping_message>(pong_addr);
    }

private:
    r::address_ptr_t pong_addr;
};

// Define message types
struct ping_message {};
struct pong_message {};

3.2 Supervision Mechanism

rotor provides an Erlang-style supervision tree to ensure system reliability. The following example shows how to create a supervisor Actor:

#include <rotor/supervisor.h>

class my_supervisor : public r::supervisor_t {
public:
    using r::supervisor_t::supervisor_t;

    void on_start() noexcept override {
        r::supervisor_t::on_start();

        // Create child Actors
        auto ping_actor = spawn_actor<ping_actor>();
        auto pong_actor = spawn_actor<pong_actor>();

        // Configure Actor relationships
        ping_actor->set_pong_addr(pong_actor->get_address());

        // Start all child Actors
        start_children();
    }
};

4. Practical Case: Building a Ping-Pong Application

4.1 Complete Example

Here is a complete Ping-Pong example implemented using the rotor framework:

#include <rotor/rotor.h>
#include <iostream>
#include <chrono>
#include <thread>

namespace r = rotor;

// Define message types
struct ping_message { int count = 0; };
struct pong_message { int count = 0; };

// pong Actor implementation
class pong_actor : public r::actor_base_t {
public:
    using r::actor_base_t::actor_base_t;

    void on_ping_message(ping_message &msg) {
        std::cout << "pong: received ping, count = " << msg.count << std::endl;
        std::this_thread::sleep_for(std::chrono::milliseconds(100));

        if (msg.count < 5) {
            send<pong_message>(get_message_source(), {msg.count + 1});
        } else {
            std::cout << "pong: finished" << std::endl;
            supervisor->do_shutdown();
        }
    }
};

// ping Actor implementation
class ping_actor : public r::actor_base_t {
public:
    using r::actor_base_t::actor_base_t;

    void set_pong_addr(const r::address_ptr_t &addr) {
        pong_addr = addr;
    }

    void on_start() noexcept override {
        r::actor_base_t::on_start();
        // Send initial ping message
        send<ping_message>(pong_addr, {0});
    }

    void on_pong_message(pong_message &msg) {
        std::cout << "ping: received pong, count = " << msg.count << std::endl;
        std::this_thread::sleep_for(std::chrono::milliseconds(100));

        if (msg.count < 5) {
            send<ping_message>(pong_addr, {msg.count});
        } else {
            std::cout << "ping: finished" << std::endl;
        }
    }

private:
    r::address_ptr_t pong_addr;
};

// Supervisor implementation
class ping_pong_supervisor : public r::supervisor_t {
public:
    using r::supervisor_t::supervisor_t;

    void on_start() noexcept override {
        r::supervisor_t::on_start();

        // Create ping and pong actors
        auto ping = spawn_actor<ping_actor>();
        auto pong = spawn_actor<pong_actor>();

        // Set Actor relationships
        ping->set_pong_addr(pong->get_address());

        // Start all Actors
        start_children();
    }
};

int main() {
    // Create and start the system
    auto supervisor = std::make_shared<ping_pong_supervisor>();
    supervisor->start();

    // Wait for the system to finish running
    std::this_thread::sleep_for(std::chrono::seconds(2));
    return 0;
}

4.2 Integration with Boost.Asio

rotor can be integrated with the Boost.Asio event loop. The following example demonstrates how to use them together:

#include <rotor/asio.hpp>
#include <boost/asio.hpp>

namespace asio = boost::asio;
namespcae ra = rotor::asio;

class timer_actor : public ra::supervisor_asio_t {
public:
    using ra::supervisor_asio_t::supervisor_asio_t;

    void on_start() noexcept override {
        ra::supervisor_asio_t::on_start();

        // Create a timer
        timer = std::make_shared<asio::steady_timer>(get_asio_executor());
        timer->expires_after(std::chrono::seconds(1));

        timer->async_wait([this](const boost::system::error_code &ec) {
            if (!ec) {
                std::cout << "Timer fired!" << std::endl;
                this->do_shutdown();
            }
        });
    }

private:
    std::shared_ptr<asio::steady_timer> timer;
};

int main() {
    asio::io_context io_context;
    auto supervisor = std::make_shared<timer_actor>(io_context);

    supervisor->start();
    io_context.run();

    return 0;
}

5. Advanced Features and Best Practices

5.1 Request-Response Pattern

rotor supports complex request-response communication patterns:

#include <rotor/request.hpp>

class worker_actor : public r::actor_base_t {
public:
    using r::actor_base_t::actor_base_t;

    void on_calculation_request(calculation_request &req) {
        // Process the request
        int result = req.value * 2;

        // Send response
        send<calculation_response>(req.source, {result}, req.request_id);
    }
};

class client_actor : public r::actor_base_t {
public:
    using r::actor_base_t::actor_base_t;

    void set_worker_addr(const r::address_ptr_t &addr) {
        worker_addr = addr;
    }

    void on_start() noexcept override {
        r::actor_base_t::on_start();

        // Send request
        request<calculation_request>(worker_addr, {42}).then(
            [this](calculation_response &resp) {
                std::cout << "Received response: " << resp.result << std::endl;
            }
        );
    }

private:
    r::address_ptr_t worker_addr;
};

// Define request-response message types
struct calculation_request { int value; };
struct calculation_response { int result; };

5.2 Error Handling and Fault Tolerance

Powerful error handling is achieved through the supervision mechanism:

class resilient_supervisor : public r::supervisor_t {
public:
    using r::supervisor_t::supervisor_t;

    customization::restart_policy_t on_child_failed(r::actor_base_t *actor, const std::error_code &ec) noexcept override {
        std::cout << "Actor failed: " << ec.message() << std::endl;

        // Decide restart policy based on error type
        if (ec == r::error_code_t::actor_crashed) {
            return customization::restart_policy_t::restart;
        } else {
            return customization::restart_policy_t::shutdown;
        }
    }

    void on_start() noexcept override {
        r::supervisor_t::on_start();

        // Create resilient child Actors
        spawn_actor<critical_service_actor>();
        spawn_actor<monitoring_actor>();

        start_children();
    }
};

6. Performance Optimization Tips

6.1 Event Loop Optimization

In actual deployment, attention should be paid to the performance characteristics of the event loop. Based on experience, in some embedded systems, it may be necessary to limit the frequency of the main event loop to avoid excessive CPU load. For example:

// In some cases, it is necessary to limit the event loop frequency
while (running) {
    handle_periodic_tasks();
    main_event();
    // Limit loop frequency to avoid 100% CPU usage
    chThdSleepMicroseconds(1000); // 1ms delay
}

6.2 Message Passing Optimization

To achieve optimal performance, consider the following optimization strategies:

  • Use move semantics to avoid message copying.
  • Use publish-subscribe patterns wisely to reduce message redundancy.
  • Choose synchronous or asynchronous message passing based on the scenario.
  • Utilize message batching to improve throughput.

7. Conclusion

As a powerful and flexible C++ Actor microframework, rotor/0.36 provides a comprehensive solution for building high-concurrency, distributed systems. Its event loop-friendly design, Erlang-style supervision tree, and high-performance messaging mechanisms enable it to perform excellently in various application scenarios.

Leave a Comment