| name | libatapp-etcd-discovery |
| description | Use when: working on libatapp etcd integration, service discovery sets, topology management, keepalive actors, watchers, node selection, or etcd_module. |
etcd Integration & Service Discovery (libatapp)
Overview
libatapp integrates with etcd v3 for:
- Service discovery: Nodes register themselves and discover others via etcd key-value store
- Topology management: Track the bus topology of all nodes in the cluster
- Keepalive: Maintain etcd lease-bound keys for presence detection
- Watch: React to node join/leave events in real time
Architecture
┌─────────────────────────────────────────────────┐
│ etcd_module (module_impl) │
│ ├── Discovery keepalive (register self) │
│ ├── Topology keepalive (register topology) │
│ ├── Discovery watcher (watch others) │
│ ├── Topology watcher (watch topology) │
│ ├── Snapshot events (initial state sync) │
│ └── Node event callbacks │
├─────────────────────────────────────────────────┤
│ etcd_discovery_set │
│ ├── Node index by id / name │
│ ├── Consistent hash ring │
│ ├── Round-robin counter │
│ └── Metadata filter │
├─────────────────────────────────────────────────┤
│ etcd_discovery_node │
│ ├── Node info (id, name, hostname, pid) │
│ ├── Version tracking │
│ └── Gateway address rotation │
├─────────────────────────────────────────────────┤
│ etcd_cluster (HTTP client) │
│ ├── KV get/set/del │
│ ├── Watch (long-poll) │
│ ├── Lease grant/renew/revoke │
│ ├── Keepalive management │
│ ├── Watcher management │
│ └── Stats tracking │
├─────────────────────────────────────────────────┤
│ etcd_keepalive │
│ └── Periodic set with lease + checker fn │
├─────────────────────────────────────────────────┤
│ etcd_watcher │
│ └── Key range watch + revision tracking │
├─────────────────────────────────────────────────┤
│ etcd_packer │
│ └── JSON ⇄ proto, base64, key range prefix │
└─────────────────────────────────────────────────┘
etcd Cluster Client (etcd_cluster)
Initialization
auto curl_mgr = atfw::util::network::http_request::create_curl_multi(...);
etcd_cluster cluster;
cluster.init(curl_mgr);
cluster.set_conf_hosts({"http://127.0.0.1:2379"});
cluster.set_conf_ssl_client_cert(cert_path);
cluster.set_conf_ssl_client_key(key_path);
cluster.set_conf_ssl_ca_cert(ca_path);
KV Operations
auto req = cluster.create_request_kv_get(key);
auto req = cluster.create_request_kv_get(key, range_end);
auto req = cluster.create_request_kv_set(key, value, true);
auto req = cluster.create_request_kv_del(key);
auto req = cluster.create_request_kv_del(key, range_end);
Watch
auto req = cluster.create_request_watch(
key,
range_end,
start_revision,
prev_kv,
progress_notify
);
Lease
int64_t lease_id = cluster.get_lease();
Stats
const auto &stats = cluster.get_stats();
stats.sum_error_requests;
stats.continue_error_requests;
stats.sum_success_requests;
stats.continue_success_requests;
stats.sum_create_requests;
Cluster Events
auto handle = cluster.add_on_event_up([](etcd_cluster &) {
}, true);
auto handle = cluster.add_on_event_down([](etcd_cluster &) {
});
cluster.remove_on_event_up(handle);
Lifecycle
cluster.tick();
cluster.is_available();
cluster.close(wait, revoke_lease);
cluster.reset();
Discovery Node (etcd_discovery_node)
Represents a single discovered service node:
auto node = std::make_shared<etcd_discovery_node>();
node->copy_from(node_info_protobuf);
node->get_discovery_info().id();
node->get_discovery_info().name();
node->get_discovery_info().hostname();
node->get_discovery_info().pid();
node->get_version();
node->next_gateway_addr();
Discovery Set (etcd_discovery_set)
A collection of discovery nodes with multiple selection strategies.
Node Selection
etcd_discovery_set &discovery = etcd_module->get_global_discovery();
auto node = discovery.get_node_by_id(0x12345678);
auto node = discovery.get_node_by_name("my_service");
auto node = discovery.get_node_by_consistent_hash(hash_key);
auto node = discovery.get_node_by_round_robin();
auto node = discovery.get_node_by_random();
atapp::protocol::atapp_metadata filter;
filter.set_area_id(1001);
auto node = discovery.get_node_by_round_robin(&filter);
Consistent Hash
The discovery set maintains a consistent hash ring for stable key→node mapping:
- Nodes are placed on the ring at multiple virtual positions
get_node_by_consistent_hash(key) finds the nearest node clockwise from the hash of key
- Adding/removing nodes only redistributes keys near the affected positions
std::vector<node_hash_type> output(3);
size_t count = discovery.lower_bound_node_hash_by_consistent_hash(
output, key_hash, &metadata_filter
);
Sorted Node Access
const auto &sorted = discovery.get_sorted_nodes();
auto it = discovery.lower_bound_sorted_nodes(id, name);
auto it = discovery.upper_bound_sorted_nodes(id, name);
Metadata Filtering
All selection methods accept const metadata_type *metadata for filtering:
bool match = metadata_equal_type::filter(rule_metadata, node_metadata);
The filter checks specific fields in atapp::protocol::atapp_metadata (e.g., area_id, region).
Node Management
discovery.add_node(node_ptr);
discovery.remove_node(node_ptr);
discovery.remove_node(id);
discovery.remove_node(name);
discovery.empty();
etcd Keepalive (etcd_keepalive)
Binds a key to the cluster's lease and periodically refreshes its value:
auto keepalive = std::make_shared<etcd_keepalive>(cluster, etcd_path_key);
keepalive->set_value(serialized_node_info);
keepalive->set_checker(checker_fn);
cluster.add_keepalive(keepalive);
cluster.remove_keepalive(keepalive);
The keepalive:
- Sets the key with the cluster's lease (auto-deleted if lease expires)
- Periodically re-sets the value (to handle etcd compaction or restarts)
- Calls checker function before each set to verify the value
etcd Watcher (etcd_watcher)
Watches a key range for put/delete events:
auto watcher = std::make_shared<etcd_watcher>(
cluster, key_prefix, range_end, event_callback
);
cluster.add_watcher(watcher);
cluster.remove_watcher(watcher);
The watcher:
- Creates a long-poll watch request
- Tracks etcd revision for consistency
- Fires callbacks for each event in order
- Re-establishes watch if connection drops
etcd Module (etcd_module)
The etcd_module integrates etcd with the app lifecycle as a module_impl.
Configuration
etcd:
hosts:
- "http://127.0.0.1:2379"
path: "/atapp/services/my_cluster"
authorization: ""
ssl:
enable: false
cert: ""
key: ""
ca_cert: ""
Module Lifecycle
init() → Create etcd_cluster, set up discovery/topology keepalives and watchers
tick() → cluster.tick(), process pending watchers/keepalives
reload() → Update config, reconnect if hosts changed
stop() → Close watchers, revoke keepalives
timeout() → Force close
Discovery & Topology
The module manages two data sets:
| Set | Storage | Key Pattern | Purpose |
|---|
| Global Discovery | etcd_discovery_set | {path}/by_id/{id} | All known service nodes |
| Topology Info | unordered_map<id, ...> | {path}/topology/{id} | Bus topology of nodes |
Key APIs
etcd_discovery_set &disc = etcd_module->get_global_discovery();
const auto &topo = etcd_module->get_topology_info_set();
auto handle = etcd_module->add_on_node_discovery_event(
[](etcd_discovery_action_t action, const etcd_discovery_node::ptr_t &node) {
if (action == etcd_discovery_action_t::kPut) {
} else if (action == etcd_discovery_action_t::kDelete) {
}
}
);
etcd_module->remove_on_node_event(handle);
auto handle = etcd_module->add_on_topology_info_event(topology_callback);
etcd_module->remove_on_topology_info_event(handle);
Keepalive Actors
The module provides a simplified API for custom keepalive registrations:
std::string value = serialize_my_data();
auto keepalive = etcd_module->add_keepalive_actor(value, custom_etcd_path);
etcd_module->remove_keepalive_actor(keepalive);
Update Notifications
When local node state changes, notify the module to refresh keepalive values:
etcd_module->set_maybe_update_keepalive_discovery_value();
etcd_module->set_maybe_update_keepalive_topology_value();
etcd_module->set_maybe_update_keepalive_discovery_area();
etcd_module->set_maybe_update_keepalive_discovery_metadata();
Snapshot Events
For initial state synchronization on startup:
auto handle = etcd_module->add_on_load_discovery_snapshot(callback);
auto handle = etcd_module->add_on_discovery_snapshot_loaded(callback);
bool has = etcd_module->has_discovery_snapshot();
Raw etcd Access
etcd_cluster &cluster = etcd_module->get_raw_etcd_ctx();
auto req = cluster.create_request_kv_get("/my/custom/key");
etcd Path Queries
etcd_module->get_discovery_by_id_path();
etcd_module->get_discovery_by_name_path();
etcd_module->get_topology_path();
etcd Packer (etcd_packer)
Utility functions for etcd data serialization:
etcd_packer::pack(const google::protobuf::Message &msg, std::string &output);
etcd_packer::unpack(const std::string &input, google::protobuf::Message &msg);
etcd_packer::get_key_range_end(const std::string &key);
etcd_packer::base64_encode(input, output);
etcd_packer::base64_decode(input, output);
etcd_packer::parse_int(string_view, int64_t &);
Testing etcd Integration
Tests Requiring etcd
Tests in atapp_etcd_cluster_test.cpp and atapp_etcd_module_test.cpp require a running etcd instance.
Quick Start with setup-etcd Scripts
Use the ci/etcd/setup-etcd scripts to download and start a local etcd:
bash ci/etcd/setup-etcd.sh start
export ATAPP_UNIT_TEST_ETCD_HOST="http://127.0.0.1:12379"
./atapp_unit_test -r atapp_etcd_cluster
./atapp_unit_test -r atapp_etcd_module
bash ci/etcd/setup-etcd.sh stop
# Windows (PowerShell)
.\ci\etcd\setup-etcd.ps1 -Command start
$env:ATAPP_UNIT_TEST_ETCD_HOST = "http://127.0.0.1:12379"
./atapp_unit_test.exe -r atapp_etcd_cluster
./atapp_unit_test.exe -r atapp_etcd_module
.\ci\etcd\setup-etcd.ps1 -Command stop
Other commands: download (download only), cleanup (stop + delete), status (check health).
Options: --work-dir DIR, --client-port PORT, --peer-port PORT, --etcd-version VER.
Manual etcd Setup
If you already have etcd running elsewhere:
export ATAPP_UNIT_TEST_ETCD_HOST="http://127.0.0.1:2379"
./atapp_unit_test -r atapp_etcd_cluster
./atapp_unit_test -r atapp_etcd_module
If ATAPP_UNIT_TEST_ETCD_HOST is not set, these tests are skipped (not failed).
Tests Not Requiring etcd
Discovery set and packer tests work without etcd:
./atapp_unit_test -r atapp_discovery
./atapp_unit_test -r atapp_etcd_packer
Mock Discovery in Tests
For tests that don't use a real etcd, inject discovery nodes directly:
atapp::protocol::atapp_discovery node_info;
node_info.set_id(0x201);
node_info.set_name("test_node");
node_info.set_hostname("localhost");
auto node = std::make_shared<etcd_discovery_node>();
node->copy_from(node_info);
etcd_module->get_global_discovery().add_node(node);
Discovery Set Unit Tests
./atapp_unit_test -r atapp_discovery
Common Pitfalls
-
etcd not available: Tests that require etcd check ATAPP_UNIT_TEST_ETCD_HOST. If not set, tests are skipped (not failed).
-
Stale discovery nodes: Discovery nodes have monotonic versions. If an update arrives with a lower version, it's ignored. Use discovery_node_version_update test as reference.
-
Key range prefix: Use etcd_packer::get_key_range_end() to compute the correct range end for prefix queries. Don't manually compute it — edge cases with \xff bytes exist.
-
Lease expiry: If the app stops refreshing the lease (e.g., frozen by debugger), etcd deletes all lease-bound keys. Other nodes will see the node as offline.
-
Snapshot timing: On startup, the module loads a snapshot of all existing discovery nodes before starting the watcher. Events between snapshot load and watcher start are reconciled automatically.
-
Metadata filter semantics: The filter requires ALL non-default fields in the rule to match. An empty filter matches everything.