08 — E2E Client¶
← Error Handling | Table Of Contents | Next: Engine Adapters →
The e2e client is a package for writing server e2e, smoke, and perf scenarios concisely as a coroutine flow. It's not the default API for a general game engine client.
Installation¶
find_package(zlink-stream-e2e-client CONFIG REQUIRED)
target_link_libraries(my_scenario_test PRIVATE
zlink::stream_connector
zlink::stream_e2e_client
)
task_t and async() are visible only when the zlink::stream_e2e_client target is included and
linked.
Basic Flow¶
#include <zlink/stream_e2e_client.hpp>
namespace ze = zlink::stream_e2e_client;
namespace zsc = zlink::stream_connector;
zsc::task_t<void> run_scenario(zsc::connector_t& connector)
{
auto client = ze::use(connector);
auto connected = co_await client.connect().async();
if (!connected) {
co_return;
}
auto auth = co_await client
.request(auth_request_t{"player-1", "tok-abc123"})
.packet_name("auth.login")
.async<auth_reply_t>();
if (!auth) {
co_await client.close().async();
co_return;
}
auto session_id = auth.value().session_id;
co_await client
.send(enter_world_t{session_id, "zone-12"})
.packet_name("world.enter")
.async();
co_await client.close().async();
}
async() doesn't call the blocking submit(). It registers a core callback completion, and once
that completes, resumes the coroutine according to the connector's delivery policy. While
co_awaiting, the worker thread isn't tied to this coroutine.
The co_await Result¶
The result of co_await is always a result_t<T>. No exception is thrown.
auto reply = co_await client
.request(ping_t{"player-1", seq})
.timeout(std::chrono::seconds{3})
.async<pong_t>();
if (!reply) {
// reply.error_code() == zsc::error_code_t::request_timeout
co_return;
}
wait_for Coroutine¶
auto event = co_await client
.wait_for<server_event_t>()
.packet_name("server.event")
.timeout(std::chrono::seconds{10})
.async();
Overlapping A Request And A Wait¶
If you register a wait for a push with start() first, then send the request, the wait can catch a
push packet arriving together with the reply.
auto wait_task = client
.wait_for<server_push_t>()
.packet_name("match.push")
.timeout(std::chrono::seconds{10})
.async();
wait_task.start(); // register the wait first
auto reply = co_await client
.request(match_request_t{"match-7f3a"})
.async<match_reply_t>();
auto push = co_await std::move(wait_task);
Handling Timeout And Close¶
| Situation | error_code |
|---|---|
| Timeout expired | request_timeout error code |
| Connector close | closed |
| Transport dropped | disconnected |
| Coroutine task destroyed | canceled |
If close() is called while pending coroutines exist, those coroutines complete with closed.
Immediate Dispatch Mode¶
In the e2e client and perf client, using immediate mode means you don't need to call dispatch()
directly.
The Perf Scenario Pattern¶
A pattern for running 5,000 connectors with a small number of worker threads:
namespace ze = zlink::stream_e2e_client;
namespace zsc = zlink::stream_connector;
zsc::task_t<void> perf_client_loop(zsc::connector_t& connector, int client_id)
{
auto client = ze::use(connector);
auto connected = co_await client.connect().async();
if (!connected) { co_return; }
int seq = 0;
while (running) {
auto reply = co_await client
.request(ping_t{client_id, ++seq})
.timeout(std::chrono::seconds{1})
.async<pong_t>();
record(reply);
}
co_await client.close().async();
}
Since the worker thread isn't tied to this coroutine while co_awaiting, thousands of concurrent
coroutines can be processed by a small number of threads.
See 11 — Performance Testing for details.