| name | libatapp-module-connector |
| description | Use when: writing libatapp modules, connectors, endpoint management, connection handles, message routing, suspend_stop logic, or multi-node tests. |
Module System & Connector Architecture (libatapp)
Module System
Module Lifecycle
Modules inherit atframework::atapp::module_impl and are registered with app.add_module(ptr).
Lifecycle call order:
on_bind() ← Module added to app (get_app() available after)
│
setup(conf) ← Config loaded, before init (can modify conf)
│
init() ← Pure virtual; must implement (return 0 = success)
│
ready() ← All modules initialized; app fully ready
│
┌─── tick loop ───────────────────────────────────────────┐
│ tick() ← Called every timer.tick_interval │
│ Return > 0 = busy (prevents sleep) │
│ prereload() ← Before config reload (if triggered) │
│ reload() ← After config reloaded │
│ setup_log() ← Log config changed │
└─────────────────────────────────────────────────────────┘
│
stop() ← App stopping; return non-zero = async stop
│
timeout() ← Called if stop exceeds deadline
│
cleanup() ← Final resource release
│
on_unbind() ← Module removed from app
Writing a Module
#include <atframe/atapp_module_impl.h>
class my_module : public atframework::atapp::module_impl {
public:
int init() override {
return 0;
}
int reload() override {
return 0;
}
int stop() override {
if (!all_done()) {
suspend_stop(std::chrono::seconds(30), [this]() {
return all_done();
});
return 1;
}
return 0;
}
int timeout() override {
force_close_everything();
return 0;
}
const char* name() const override { return "my_module"; }
int tick() override {
int busy = process_pending();
return busy;
}
};
suspend_stop
Delays module shutdown until a condition is met or timeout expires:
suspend_stop(std::chrono::seconds(30), []() -> bool {
return condition_met;
});
- The check function is called each tick during stop phase
- If timeout expires without returning true,
timeout() is called
- Multiple modules can suspend_stop concurrently; app waits for all
Module Access Patterns
get_app()
get_app()->get_id()
get_app()->get_app_name()
get_app()->get_origin_configure()
get_app()->get_bus_node()
get_app()->is_running()
get_app()->is_closing()
get_app()->get_last_tick_time()
Connector Architecture
Connector Hierarchy
atapp_connector_impl (base)
├── atapp_connector_atbus (libatbus message bus)
└── atapp_connector_loopback (self-message delivery)
Connector Interface
class atapp_connector_impl {
public:
virtual uint32_t get_address_type(const channel_address_t &addr) const noexcept = 0;
virtual int32_t on_start_listen(const channel_address_t &addr);
virtual int32_t on_start_connect(
const etcd_discovery_node &discovery,
atapp_endpoint &endpoint,
const channel_address_t &addr,
const atapp_connection_handle::ptr_t &handle);
virtual int32_t on_close_connection(atapp_connection_handle &handle);
virtual int32_t on_send_forward_request(
atapp_connection_handle *handle,
int32_t type,
uint64_t *msg_sequence,
gsl::span<const unsigned char> data,
const atapp::protocol::atapp_metadata *metadata);
virtual void on_receive_forward_response(...);
virtual void on_discovery_event(etcd_discovery_action_t, const etcd_discovery_node::ptr_t &);
};
Address Types (bitfield)
address_type_t:
kNone = 0x0000
kDuplex = 0x0001
kSimplex = 0x0002
kLocalHost = 0x0004
kLocalProcess = 0x0008
Address scheme to type mapping (atbus connector):
| Scheme | Address Type |
|---|
mem:// | kDuplex | kLocalProcess |
shm:// | kDuplex | kLocalHost |
unix:// | kDuplex | kLocalHost |
ipv4://, ipv6://, dns:// | kDuplex |
atcp:// | kDuplex |
Writing a Custom Connector
class my_connector : public atframework::atapp::atapp_connector_impl {
public:
uint32_t get_address_type(const channel_address_t &addr) const noexcept override {
if (addr.scheme == "myproto") {
return address_type_t::kDuplex;
}
return address_type_t::kNone;
}
int32_t on_start_connect(
const etcd_discovery_node &discovery,
atapp_endpoint &endpoint,
const channel_address_t &addr,
const atapp_connection_handle::ptr_t &handle) override {
return 0;
}
int32_t on_send_forward_request(
atapp_connection_handle *handle,
int32_t type,
uint64_t *msg_sequence,
gsl::span<const unsigned char> data,
const atapp::protocol::atapp_metadata *metadata) override {
return 0;
}
};
ATBus Connector Routing
The atapp_connector_atbus performs topology-aware routing in try_connect_to():
Target resolution:
1. Is target self? → loopback connector
2. Is target a known peer? → direct atbus connection
3. Same upstream parent? → direct connection attempt
4. Target is upstream? → connect directly to upstream
5. Target is downstream? → route down the tree
6. Otherwise: → forward to upstream proxy
Reconnect Logic
- Reconnect uses exponential backoff
- Configurable:
reconnect_tick_interval (base interval), reconnect_max_try_times (max retries)
- Each failed attempt increases the backoff multiplier
- Timer is managed via
jiffies_timer_watcher_t (can cancel with remove_custom_timer)
Handle Map
The atbus connector maintains a handle map keyed by bus_id:
Connection Handle
atapp_connection_handle wraps an individual connection:
struct flags_t {
kReady = 0,
kClosing = 1,
};
handle->check_flag(flags_t::kReady)
handle->set_flag(flags_t::kReady, true)
handle->get_private_data<MyType>()
handle->set_private_data(ptr, destructor_fn)
handle->on_destroy = callback
Endpoint Lifecycle
atapp_endpoint represents a remote node with message queuing:
Discovery event (node discovered)
│
▼
Create endpoint → bind discovery_node
│
▼
Connector attempts connection → creates connection_handle
│
▼
Connection ready (handle.kReady = true)
├── Retry pending messages → send via connector
└── New messages sent directly
│
▼
Connection lost
├── New messages queued as pending
└── Connector reconnects (if applicable)
│
▼
GC timeout → endpoint removed (if no activity)
Pending Message Queue
When no ready connection exists, messages are queued:
struct pending_message_t {
raw_time_t expired_timepoint;
int32_t type;
uint64_t message_sequence;
std::vector<unsigned char> data;
std::unique_ptr<atapp::protocol::atapp_metadata> metadata;
};
push_forward_message() — queue a message
retry_pending_messages() — send queued messages when connection becomes ready
get_pending_message_count() / get_pending_message_size() — inspect queue
- Messages expire based on
expired_timepoint; expired messages are discarded
Multi-Node Test Patterns
Three-Node Setup (Direct Connect)
struct direct_three_node_apps {
app node1;
app node2;
app upstream;
void full_setup_and_connect() {
}
};
Three-Node Setup (Upstream Proxy)
struct three_node_apps {
app node1;
app upstream;
app node3;
};
Event Loop Helpers
void pump_until(std::function<bool()> condition, std::chrono::milliseconds timeout) {
auto deadline = std::chrono::steady_clock::now() + timeout;
while (!condition() && std::chrono::steady_clock::now() < deadline) {
for (auto &a : apps) {
a.run_noblock();
}
CASE_THREAD_SLEEP_MS(8);
}
}
app::set_sys_now(future_time_point);
app.tick();
Testing Caveats
-
Shared uv_default_loop(): When multiple apps use init(nullptr, ...), they share the default libuv loop. run_noblock() fires timers for ALL apps. Use tick() in time-advance sections.
-
set_sys_now() is static: Affects ALL app instances. Set large bus timeouts (e.g., first_idle_timeout: 3600) in test configs to prevent atbus idle disconnect.
-
Jiffies timer + set_sys_now(): Only one timer callback fires per tick() call when time is jumped. Use multiple set_sys_now() + tick() rounds for multi-timer scenarios.
-
Discovery injection: Mock discovery by injecting nodes directly:
auto node = std::make_shared<etcd_discovery_node>();
node->copy_from(discovery_info);
etcd_module->get_global_discovery().add_node(node);
Common Pitfalls
-
Module init order: init() is called in registration order. If module B depends on module A, register A first.
-
Pending message overflow: If connection never becomes ready and messages have long expiry, memory grows. Monitor get_pending_message_size().
-
Connector address overlap: If two connectors handle the same scheme, the first registered one wins. Check get_address_type() returns kNone for schemes you don't handle.
-
stop() return value: Return 0 = stop complete. Return non-zero = will be called again. Forgetting to eventually return 0 causes the app to hang until timeout.
-
Timer lazy init: add_custom_timer() before the first tick() may fail because the jiffies timer controller isn't initialized yet. Call is safe after first tick() or after process_custom_timers() runs.