test(net): 去掉 net 测试的 flaky,顺带把整套 ctest 从 78.7s 降到 8.7s
全量 ctest 连续两次各挂一个 net 测试,且挂的不是同一个,压测后定位到
四个互相独立的原因,全部在测试自身,产品代码未改动。
1. accept_loopback(): poll() 的 `<= 0` 把 EINTR 当成对端从未连接,且单片
500ms 超时在有负载时不够。50 轮压测里 4 次失败全部源于此。改为 EINTR
重试 + 5s 截止时间(listener 本就非阻塞,超时后 accept 不会挂住)。
2. :570 的 pump 循环条件抄自上面的死端口用例,但此处 connect 会成功,
stage 先经过 LoggingIn,循环提前一步退出。
3. 同文件 5 处手写 pump 循环只给 10-20ms 等真实 loopback socket。新增
pump_until(session, want, max_ms=2000) 统一替换,等的是断言真正关心
的那个状态。
4. net_loopback_test 的 10 处 run(..., N, []{return false;}) 是固定空转
而非等待条件。服务端批量收包改用该批最后一个包的标志作条件(TCP 有序),
客户端事件用本地累加器在谓词里 drain。
提速:两处 this-*-host-must-not-resolve.invalid 换成带空标签的名字。本机
解析器不按 RFC 6761 本地拒绝 .invalid,每次发往上游等满 30s 超时;空标签
让 getaddrinfo 在语法层返回同一个 EAI_NONAME,实测 <10ms。
net_classic_session_test 61.2s -> 3.5s;loopback 改条件退出后 0.94s。
验证:net_classic_session_test 100/100(修复前 50 轮挂 4 轮);
net_loopback_test 100/100(修复前单独跑第 7 轮即挂);全量 ctest 27/27;
port_map.py check 0 error;8 个 pack key 值均未入库。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
#include "../src/net/classic/classic_session.h"
|
||||
|
||||
#include <arpa/inet.h>
|
||||
#include <cerrno>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <fcntl.h>
|
||||
@@ -52,6 +53,21 @@ static std::vector<uint8_t> drain(ClassicSession &s) {
|
||||
return drain_stream(s.stream());
|
||||
}
|
||||
|
||||
// Pump until the session reaches `want`, or give up after `max_ms`.
|
||||
//
|
||||
// Every caller below is waiting on a real loopback socket: a nonblocking
|
||||
// connect completing, a connect being refused, or a peer's FIN arriving. None
|
||||
// of that is instantaneous, and the old hand-rolled loops gave it 10-20ms,
|
||||
// which made these checks fail intermittently on a loaded machine. Waiting on
|
||||
// the state the check is about (rather than on "still Connecting") also avoids
|
||||
// stopping one step early when the stage passes through an intermediate owner.
|
||||
static void pump_until(ClassicSession &s, mtnet::INetSession::Stage want, int max_ms = 2000) {
|
||||
for (int i = 0; i < max_ms && s.stage() != want; ++i) {
|
||||
s.pump();
|
||||
::usleep(1000);
|
||||
}
|
||||
}
|
||||
|
||||
static std::string cjk_name(int count) {
|
||||
std::string value;
|
||||
for (int i = 0; i < count; ++i) {
|
||||
@@ -96,12 +112,29 @@ static int listen_loopback(uint16_t &port) {
|
||||
}
|
||||
|
||||
static bool accept_loopback(int listener, int &accepted) {
|
||||
pollfd pfd{listener, POLLIN, 0};
|
||||
if (::poll(&pfd, 1, 500) <= 0) {
|
||||
return false;
|
||||
// Two things made this the single largest source of flakiness in this suite.
|
||||
// poll() returns -1/EINTR whenever a signal lands, which the old `<= 0` test
|
||||
// counted as "the peer never connected"; and one flat 500ms slice is not
|
||||
// always enough for a loopback connect on a loaded machine. Retry through
|
||||
// EINTR and give the connect a real deadline instead.
|
||||
constexpr int deadline_ms = 5000;
|
||||
constexpr int slice_ms = 250;
|
||||
for (int waited = 0; waited < deadline_ms;) {
|
||||
pollfd pfd{listener, POLLIN, 0};
|
||||
const int rc = ::poll(&pfd, 1, slice_ms);
|
||||
if (rc > 0) {
|
||||
accepted = ::accept(listener, nullptr, nullptr);
|
||||
return accepted >= 0;
|
||||
}
|
||||
if (rc < 0) {
|
||||
if (errno == EINTR) {
|
||||
continue; // interrupted, not expired
|
||||
}
|
||||
return false;
|
||||
}
|
||||
waited += slice_ms;
|
||||
}
|
||||
accepted = ::accept(listener, nullptr, nullptr);
|
||||
return accepted >= 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
int main() {
|
||||
@@ -472,11 +505,7 @@ int main() {
|
||||
CHECK(auth_connect_failure.connect("127.0.0.1", dead_port,
|
||||
"127.0.0.1", 1, "admin", "secret"),
|
||||
"auth connect-failure fixture starts nonblocking connection");
|
||||
for (int i = 0; i < 20 &&
|
||||
auth_connect_failure.stage() == mtnet::INetSession::Stage::Connecting; ++i) {
|
||||
auth_connect_failure.pump();
|
||||
::usleep(1000);
|
||||
}
|
||||
pump_until(auth_connect_failure, mtnet::INetSession::Stage::Offline);
|
||||
CHECK(auth_connect_failure.stage() == mtnet::INetSession::Stage::Offline,
|
||||
"auth connect failure returns to the 40250 Offline owner");
|
||||
uint16_t retry_port = 0;
|
||||
@@ -539,10 +568,16 @@ int main() {
|
||||
// CAccountConnector::Connect routes a synchronous socket/address failure
|
||||
// through OnConnectFailure and then __OfflineState_Set. It must be a
|
||||
// retryable Offline owner, not a terminal Failed stage.
|
||||
//
|
||||
// The unresolvable host has an empty label on purpose. getaddrinfo() rejects
|
||||
// that on syntax and returns EAI_NONAME without asking a resolver; a ".invalid"
|
||||
// name is supposed to be refused locally too (RFC 6761) but resolvers here hand
|
||||
// it upstream and wait out the full 30s timeout, which is where this suite used
|
||||
// to spend most of its wall clock.
|
||||
{
|
||||
ClassicSession auth_immediate_failure;
|
||||
const bool connected = auth_immediate_failure.connect(
|
||||
"this-auth-host-must-not-resolve.invalid", 1,
|
||||
"this-auth-host..must-not-resolve", 1,
|
||||
"127.0.0.1", 1, "admin", "secret");
|
||||
CHECK(!connected, "invalid auth address reports a synchronous connect failure");
|
||||
CHECK(auth_immediate_failure.stage() == mtnet::INetSession::Stage::Offline,
|
||||
@@ -567,10 +602,10 @@ int main() {
|
||||
if (accepted >= 0) {
|
||||
::close(accepted);
|
||||
}
|
||||
for (int i = 0; i < 20 && auth_close.stage() == mtnet::INetSession::Stage::Connecting; ++i) {
|
||||
auth_close.pump();
|
||||
::usleep(1000);
|
||||
}
|
||||
// Unlike the dead-port fixture above, this connect succeeds, so the
|
||||
// stage goes Connecting -> LoggingIn first and only reaches Offline
|
||||
// once the peer's FIN is read.
|
||||
pump_until(auth_close, mtnet::INetSession::Stage::Offline);
|
||||
CHECK(auth_close.stage() == mtnet::INetSession::Stage::Offline,
|
||||
"auth peer close returns to the 40250 Offline owner");
|
||||
auth_close.disconnect();
|
||||
@@ -828,9 +863,14 @@ int main() {
|
||||
"auth success starts the replacement game transport without processing it in the same frame");
|
||||
CHECK(replacement_phase_sent,
|
||||
"replacement game fixture sends PHASE_LOGIN before the handoff callback returns");
|
||||
retry_handoff.pump();
|
||||
// The check above already pinned down "not in the same frame". Here we
|
||||
// only need a later pump to pick it up, and that needs the replacement
|
||||
// transport's nonblocking connect to have completed and PHASE_LOGIN to
|
||||
// have crossed the loopback — a single pump is not guaranteed to see
|
||||
// both.
|
||||
pump_until(retry_handoff, mtnet::INetSession::Stage::LoggingIn);
|
||||
CHECK(retry_handoff.stage() == mtnet::INetSession::Stage::LoggingIn,
|
||||
"replacement PHASE_LOGIN is processed on the following pump");
|
||||
"replacement PHASE_LOGIN is processed on a following pump");
|
||||
|
||||
if (auth_connection >= 0) {
|
||||
::close(auth_connection);
|
||||
@@ -1896,10 +1936,7 @@ int main() {
|
||||
if (accepted >= 0) {
|
||||
::close(accepted);
|
||||
}
|
||||
for (int i = 0; i < 10 && pre_game_close.stage() != mtnet::INetSession::Stage::LoggingIn; ++i) {
|
||||
pre_game_close.pump();
|
||||
::usleep(1000);
|
||||
}
|
||||
pump_until(pre_game_close, mtnet::INetSession::Stage::LoggingIn);
|
||||
CHECK(pre_game_close.stage() == mtnet::INetSession::Stage::LoggingIn,
|
||||
"peer close before PHASE_GAME returns to LoginPhase");
|
||||
CHECK(!pre_game_close.phase_closed(),
|
||||
@@ -1931,10 +1968,7 @@ int main() {
|
||||
if (in_game_accepted >= 0) {
|
||||
::close(in_game_accepted);
|
||||
}
|
||||
for (int i = 0; i < 10 && in_game_close.stage() != mtnet::INetSession::Stage::LoggingIn; ++i) {
|
||||
in_game_close.pump();
|
||||
::usleep(1000);
|
||||
}
|
||||
pump_until(in_game_close, mtnet::INetSession::Stage::LoggingIn);
|
||||
CHECK(in_game_close.stage() == mtnet::INetSession::Stage::LoggingIn,
|
||||
"in-game peer close returns to LoginPhase");
|
||||
CHECK(in_game_close.login_retry_pending(),
|
||||
@@ -2009,10 +2043,7 @@ int main() {
|
||||
// The connection may fail synchronously (ECONNREFUSED) or on the
|
||||
// first non-blocking pump; both paths must reach LoginPhase.
|
||||
direct_failure.connect_direct_enter(0);
|
||||
for (int i = 0; i < 10 && direct_failure.stage() != mtnet::INetSession::Stage::LoggingIn; ++i) {
|
||||
direct_failure.pump();
|
||||
::usleep(1000);
|
||||
}
|
||||
pump_until(direct_failure, mtnet::INetSession::Stage::LoggingIn);
|
||||
CHECK(direct_failure.stage() == mtnet::INetSession::Stage::LoggingIn,
|
||||
"DirectEnter connect failure returns to LoginPhase");
|
||||
CHECK(direct_phase_close,
|
||||
@@ -2059,7 +2090,7 @@ int main() {
|
||||
connect_failure_reason = reason;
|
||||
};
|
||||
CHECK(ordinary_failure.connect("127.0.0.1", auth_port,
|
||||
"this-game-host-must-not-resolve.invalid", 1, "admin", "secret"),
|
||||
"this-game-host..must-not-resolve", 1, "admin", "secret"),
|
||||
"ordinary game-connect failure fixture opens the auth transport");
|
||||
int accepted = -1;
|
||||
CHECK(accept_loopback(auth_listener, accepted),
|
||||
|
||||
Reference in New Issue
Block a user