qb
2.0.0.0
C++17 Actor Framework
|
Practical recipes and C++ examples for implementing common and effective actor-based design patterns with the QB Framework.
This cookbook provides practical recipes and conceptual C++ examples for implementing common design patterns in your QB actor-based applications. These patterns help structure your system, manage state, and handle interactions effectively. For information about Service Actors and Periodic Callbacks, please refer to the QB-Core: Common Actor Patterns & Utilities page.
Actors naturally model entities with distinct states and transitions triggered by events. The QB-Core: Common Actor Patterns & Utilities page provides a detailed explanation and example of implementing FSMs.
Key Idea: An actor's member variables hold its current state. Event handlers (on(Event&) methods) implement state transition logic based on the current state and the received event.
This pattern decouples message senders (publishers) from receivers (subscribers) using a central Broker actor that manages topics and message distribution.
Conceptual Flow: ```text +----------—+ +--------------—+ +----------—+ | Publisher A |----->| PublishReq(T1) |----->| BrokerActor | +----------—+ +--------------—+ +----------—+ ^ | | 2. Broker sends | 1. Publisher sends message for Topic T1 | NotificationMsg(T1) | Manages subscriptions for T1, T2... | to relevant subscribers | +--—|----—+ +--—|----—+ | SubscriberX | | SubscriberY | | (Subscribed | | (Subscribed | | to T1) | | to T1) | +----------—+ +----------—+
+----------—+ | SubscriberZ | | (Subscribed | | to T2) | <– No message, not subscribed to T1 +----------—+ ```
Conceptual Implementation:
Events: ```cpp #include <qb/actor.h> #include <qb/event.h> #include <qb/string.h> #include <string> // For std::string in map keys if needed, or use qb::string #include <vector> #include <set> #include <map> #include <memory> // For std::shared_ptr
// Using qb::string for topic for efficiency and ABI safety in events struct SubscribeReq : qb::Event { qb::string<64> topic; }; struct UnsubscribeReq : qb::Event { qb::string<64> topic; }; struct PublishReq : qb::Event { qb::string<64> topic; std::shared_ptr<qb::string<256>> content; // Use shared_ptr for potentially large content PublishReq(const char* t, std::shared_ptr<qb::string<256>> c) : topic(t), content(c) {} }; struct NotificationMsg : qb::Event { qb::string<64> topic; std::shared_ptr<qb::string<256>> content; // Share content efficiently NotificationMsg(qb::string<64> t, std::shared_ptr<qb::string<256>> c) : topic(std::move(t)), content(c) {} }; ```
Broker Actor: ```cpp class BrokerActor : public qb::Actor { private: // Using std::string as map key here for simplicity in example; // consider qb::string or custom hash for qb::string if performance critical. std::map<std::string, std::set<qb::ActorId>> _subscriptions;
public: bool onInit() override { registerEvent<SubscribeReq>(*this); registerEvent<UnsubscribeReq>(*this); registerEvent<PublishReq>(*this); // ... KillEvent ... return true; }
void on(const SubscribeReq& event) { std::string topic_str = event.topic.c_str(); // Convert for map key if needed _subscriptions[topic_str].insert(event.getSource()); qb::io::cout() << "Broker: Actor " << event.getSource() << " subscribed to " << topic_str << ".\n"; }
void on(const UnsubscribeReq& event) { std::string topic_str = event.topic.c_str(); if (_subscriptions.count(topic_str)) { _subscriptions[topic_str].erase(event.getSource()); qb::io::cout() << "Broker: Actor " << event.getSource() << " unsubscribed from " << topic_str << ".\n"; } }
void on(const PublishReq& event) { std::string topic_str = event.topic.c_str(); qb::io::cout() << "Broker: Publishing to topic '" << topic_str << "': " << event.content->c_str() << ".\n"; if (_subscriptions.count(topic_str)) { for (qb::ActorId subscriber_id : _subscriptions[topic_str]) { // Create NotificationMsg once, pass shared_ptr to content push<NotificationMsg>(subscriber_id, event.topic, event.content); } } } // ... KillEvent handler ... }; ```
Subscriber Actor: cpp class SubscriberActor : public qb::Actor { private: qb::ActorId _broker_id; public: SubscriberActor(qb::ActorId broker) : _broker_id(broker) {} bool onInit() override { registerEvent<NotificationMsg>(*this); // ... KillEvent ... // Subscribe to a topic push<SubscribeReq>(_broker_id, "news/local"); return true; } void on(const NotificationMsg& event) { qb::io::cout() << "Subscriber [" << id() << "] on topic '" << event.topic.c_str() << "' received: " << event.content->c_str() << ".\n"; } // ... KillEvent handler could send UnsubscribeReq ... };
Actors often need to request information or an action from another actor and await a response, potentially with a timeout if the response doesn't arrive promptly.
Safely manage access to resources that are not inherently thread-safe (e.g., a database connection, a file being written sequentially) by encapsulating the resource within a dedicated Manager Actor.
Conceptual Example (Simplified Logger Manager): ```cpp #include <fstream> // For std::ofstream // Events struct LogLineEvent : qb::Event { qb::string<256> line_to_log; }; // No response needed for this simple logger
class FileLoggerManager : public qb::Actor { private: std::ofstream _log_file; qb::string<128> _file_path; public: FileLoggerManager(const char* file_path) : _file_path(file_path) {}
bool onInit() override { _log_file.open(_file_path.c_str(), std::ios::app); if (!_log_file.is_open()) { qb::io::cout() << "Logger: Failed to open " << _file_path.c_str() << "!\n"; return false; // Fail actor initialization } registerEvent<LogLineEvent>(*this); registerEvent<qb::KillEvent>(*this); qb::io::cout() << "Logger [" << id() << "] started for file: " << _file_path.c_str() << ".\n"; return true; }
void on(const LogLineEvent& event) { if (_log_file.is_open()) { _log_file << event.line_to_log.c_str() << std::endl; // endl flushes by default } }
void on(const qb::KillEvent& /*event*/) { qb::io::cout() << "Logger [" << id() << "] shutting down. Closing file.\n"; if (_log_file.is_open()) { _log_file.close(); } kill(); } // Destructor will also be called, but explicit close in KillEvent is good for clarity ~FileLoggerManager() override { if (_log_file.is_open()) { _log_file.close(); } } }; ``
These patterns offer robust solutions for structuring complex actor interactions. By combining them and adapting them to your specific needs, you can build sophisticated, scalable, and maintainable concurrent applications with the QB Actor Framework.
(Next: Explore QB Framework: Advanced Techniques & System Design for more in-depth techniques, or QB Framework: Performance Tuning Guide for optimization strategies.**)