Command Group
Load-balanced command handling with consumer groups
Overview
A command consumer group turns a single command handler into a scalable worker pool: run multiple identical instances subscribed with the same group name, and the broker load-balances each incoming command to exactly one member instead of broadcasting it to all of them. This is how you add capacity to handle a growing command volume — start more processes in the same group — without changing anything on the caller's side.
Every subscriber passes the same group alongside channel to SubscribeToCommands; the broker tracks membership and picks one live member per command. SendCommand on the caller side is unaware groups exist — it just blocks for a response, which comes back from whichever worker happened to handle it via SendCommandResponse.
Gotchas: group membership is scoped per channel — subscribers on the same channel with different group names each get their own full copy of every command (fan-out), which looks like a bug when you expected load-balancing. A slow handler still holds up the caller's timeout, since only one worker is ever picked. And if every member of the group is offline when a command arrives, the send simply fails or times out — commands aren't queued or replayed for a group that has no active listener.
Prerequisites
- KubeMQ server running on
localhost:50000 - C++ SDK installed (vcpkg or CMake FetchContent)
- C++17 compiler (GCC 9+, Clang 9+, MSVC 2019+)
Code
// Example: commands/consumer_group
//
// Demonstrates load-balanced command handling with consumer groups.
// Multiple handlers in the same group share the command workload,
// with each command delivered to exactly one handler.
//
// Channel: cpp-commands.consumer-group
// Client ID: cpp-commands-consumer-group-client
//
// Run with a KubeMQ server on localhost:50000
// (see https://docs.kubemq.io/deploy).
#include <kubemq/kubemq.h>
#include <atomic>
#include <chrono>
#include <iostream>
#include <thread>
int main() {
std::cout << "[1] Connecting to localhost:50000" << std::endl;
kubemq::ClientOptions options;
options.set_address("localhost", 50000);
options.set_client_id("cpp-commands-consumer-group-client");
auto client_result = kubemq::Client::Create(options);
if (!client_result.ok()) {
std::cerr << "[ERROR] Failed to create client: " << client_result.status().message()
<< std::endl;
return 1;
}
auto& client = *client_result;
std::string channel = "cpp-commands.consumer-group";
std::string group = "cpp-commands-worker-group";
std::atomic<bool> command_handled{false};
// Subscribe with a consumer group for load-balanced command handling.
std::cout << "[2] Subscribing to commands with group " << group << std::endl;
auto sub_result = client->SubscribeToCommands(
channel, group,
[&client, &command_handled](const kubemq::CommandReceive& cmd) {
std::cout << "[4] Worker received: body=" << cmd.body << std::endl;
auto now_epoch = std::chrono::duration_cast<std::chrono::seconds>(
std::chrono::system_clock::now().time_since_epoch())
.count();
auto reply_result = kubemq::CommandReply::Builder()
.SetRequestId(cmd.id)
.SetResponseTo(cmd.response_to)
.SetExecuted(true)
.SetExecutedAt(now_epoch)
.Build();
if (!reply_result.ok()) {
std::cerr << "[ERROR] Build reply: " << reply_result.status().message()
<< std::endl;
return;
}
auto send_status = client->SendCommandResponse(*reply_result);
if (!send_status.ok()) {
std::cerr << "[ERROR] SendCommandResponse: " << send_status.message() << std::endl;
return;
}
command_handled.store(true);
},
[](const kubemq::Status& err) {
std::cerr << "[ERROR] Group error: " << err.message() << std::endl;
});
if (!sub_result.ok()) {
std::cerr << "[ERROR] SubscribeToCommands: " << sub_result.status().message() << std::endl;
return 1;
}
auto& sub = *sub_result;
// Allow subscription to fully establish before sending.
std::this_thread::sleep_for(std::chrono::milliseconds(300));
// Send a command to the group.
std::cout << "[3] Sending command to group " << group << std::endl;
auto cmd_result = kubemq::Command::Builder()
.SetChannel(channel)
.SetBody("group-task")
.SetTimeout(std::chrono::seconds(10))
.Build();
if (!cmd_result.ok()) {
std::cerr << "[ERROR] Build command: " << cmd_result.status().message() << std::endl;
return 1;
}
auto resp_result = client->SendCommand(*cmd_result);
if (!resp_result.ok()) {
std::cerr << "[ERROR] SendCommand: " << resp_result.status().message() << std::endl;
return 1;
}
std::cout << "[5] Group response: executed=" << std::boolalpha << resp_result->executed
<< std::endl;
// Wait for the handler to finish processing.
std::this_thread::sleep_for(std::chrono::seconds(1));
// Cancel the subscription explicitly.
// Note: The Subscription destructor also calls Cancel(), but explicit
// cleanup is shown here for clarity and to match Go's defer pattern.
sub->Cancel();
std::cout << "[6] Subscription cancelled" << std::endl;
auto close_status = client->Close();
if (!close_status.ok()) {
std::cerr << "[ERROR] Close failed: " << close_status.message() << std::endl;
return 1;
}
std::cout << "[7] Client closed" << std::endl;
return 0;
}How It Works
- Subscribes to commands with a consumer group for load-balanced delivery.
- When multiple handlers share the same group, each command is handled by exactly one member.
- Sends a command and receives the response from whichever group member handled it.
- Enables horizontal scaling of command processors.
Related
Was this page helpful?