Don't block on hole punching when a client joins

CNetServer::SendHolePunchingMessage is called on the main thread (from
the lobby's XMPP handler) whenever a lobby client requests to connect.
It called StunClient::SendHolePunchingMessages, which sleeps for
fw_punch.delay (default 200 ms) after each of the fw_punch.num_msg
(default 3) messages. Freezing the main thread for ~600 ms freezes the
hosting player's game, which in turn delays the lockstep turns of every
player in the match.

Instead, queue the request to the network server worker thread (like
lobby auths) and pace the messages from CNetServerWorker::RunStep
without sleeping. As the worker now owns the whole exchange, this also
removes the concurrent use of the server's ENetHost from two threads.
Punching stops as soon as the peer connects, which also gives
num_msg = -1 (send indefinitely) a sane meaning.

Fixes: #7957

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
josue 2026-06-12 13:10:22 +02:00
parent 917275d6cb
commit 0321f6a8a7
4 changed files with 92 additions and 7 deletions

View file

@ -52,6 +52,7 @@
#include "simulation2/system/TurnManager.h"
#include <algorithm>
#include <chrono>
#include <cstring>
#include <functional>
#include <iterator>
@ -414,6 +415,7 @@ bool CNetServerWorker::RunStep()
std::vector<bool> newStartGame;
std::vector<std::pair<CStr, CStr>> newLobbyAuths;
std::vector<u32> newTurnLength;
std::vector<std::pair<CStr, u16>> newHolePunchRequests;
{
std::lock_guard<std::mutex> lock(m_WorkerMutex);
@ -424,6 +426,7 @@ bool CNetServerWorker::RunStep()
newStartGame.swap(m_StartGameQueue);
newLobbyAuths.swap(m_LobbyAuthQueue);
newTurnLength.swap(m_TurnLengthQueue);
newHolePunchRequests.swap(m_HolePunchQueue);
}
if (!newTurnLength.empty())
@ -442,6 +445,8 @@ bool CNetServerWorker::RunStep()
CheckClientConnections();
ProcessHolePunching(std::move(newHolePunchRequests));
// Process network events:
ENetEvent event;
@ -470,6 +475,13 @@ bool CNetServerWorker::RunStep()
enet_address_get_host_ip(&event.peer->address, hostname, ARRAY_SIZE(hostname));
LOGMESSAGE("Net server: Received connection from %s:%u", hostname, (unsigned int)event.peer->address.port);
// The peer connected, so the hole punching for it succeeded or wasn't needed.
std::erase_if(m_HolePunchTargets, [&](const HolePunchTarget& target)
{
return target.address.host == event.peer->address.host &&
target.address.port == event.peer->address.port;
});
// Set up a session object for this peer
const std::unique_ptr<CNetServerSession>& session{m_Sessions.emplace_back(
@ -1644,10 +1656,47 @@ CStrW CNetServerWorker::DeduplicatePlayerName(const CStrW& original)
}
}
void CNetServerWorker::SendHolePunchingMessage(const CStr& ipStr, u16 port)
void CNetServerWorker::ProcessHolePunching(std::vector<std::pair<CStr, u16>>&& newRequests)
{
if (m_Host)
StunClient::SendHolePunchingMessages(*m_Host, ipStr, port);
if (!m_Host)
return;
const std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now();
for (const std::pair<CStr, u16>& request : newRequests)
{
// A negative number of messages means punching until the peer connects.
const int numMessages{g_ConfigDB.Get("lobby.fw_punch.num_msg", 3)};
if (numMessages == 0)
continue;
ENetAddress address;
address.port = request.second;
if (enet_address_set_host(&address, request.first.c_str()) != 0)
{
LOGWARNING("Net server: Failed to resolve hole punching target %s", request.first.c_str());
continue;
}
m_HolePunchTargets.push_back({address, numMessages, now});
}
if (m_HolePunchTargets.empty())
return;
const std::chrono::milliseconds delay{g_ConfigDB.Get("lobby.fw_punch.delay", 200)};
for (HolePunchTarget& target : m_HolePunchTargets)
{
if (now < target.nextSendTime)
continue;
StunClient::SendHolePunchingMessage(*m_Host, target.address);
if (target.remainingMessages > 0)
--target.remainingMessages;
target.nextSendTime = now + delay;
}
std::erase_if(m_HolePunchTargets,
[](const HolePunchTarget& target) { return target.remainingMessages == 0; });
}
@ -1735,5 +1784,6 @@ void CNetServer::SetTurnLength(u32 msecs)
void CNetServer::SendHolePunchingMessage(const CStr& ip, u16 port)
{
m_Worker.SendHolePunchingMessage(ip, port);
std::lock_guard<std::mutex> lock(m_Worker.m_WorkerMutex);
m_Worker.m_HolePunchQueue.emplace_back(ip, port);
}

View file

@ -24,6 +24,7 @@
#include "network/NetHost.h"
#include "ps/CStr.h"
#include <chrono>
#include <ctime>
#include <js/RootingAPI.h>
#include <js/TypeDecls.h>
@ -234,7 +235,11 @@ private:
*/
void CheckClientConnections();
void SendHolePunchingMessage(const CStr& ip, u16 port);
/**
* Turn new hole punching requests from the game thread into targets and
* send the due hole punching messages without blocking.
*/
void ProcessHolePunching(std::vector<std::pair<CStr, u16>>&& newRequests);
/**
* Internal script context for (de)serializing script messages,
@ -319,6 +324,22 @@ private:
*/
std::time_t m_LastConnectionCheck{0};
/**
* A peer which should receive hole punching messages until it connects.
*/
struct HolePunchTarget
{
ENetAddress address;
/// A negative number means sending until the peer connects.
int remainingMessages;
std::chrono::steady_clock::time_point nextSendTime;
};
/**
* The peers currently being sent hole punching messages.
*/
std::vector<HolePunchTarget> m_HolePunchTargets;
private:
// Thread-related stuff:
@ -344,6 +365,7 @@ private:
std::vector<bool> m_StartGameQueue;
std::vector<std::pair<CStr, CStr>> m_LobbyAuthQueue;
std::vector<u32> m_TurnLengthQueue;
std::vector<std::pair<CStr, u16>> m_HolePunchQueue;
};
/**

View file

@ -1,4 +1,4 @@
/* Copyright (C) 2025 Wildfire Games.
/* Copyright (C) 2026 Wildfire Games.
* Copyright (C) 2013-2016 SuperTuxKart-Team.
* This file is part of 0 A.D.
*
@ -371,6 +371,11 @@ void SendHolePunchingMessages(ENetHost& enetClient, const std::string& serverAdd
}
}
void SendHolePunchingMessage(ENetHost& enetClient, const ENetAddress& addr)
{
SendStunRequest(enetClient, addr);
}
bool FindLocalIP(CStr& ip)
{
// Open an UDP socket.

View file

@ -1,4 +1,4 @@
/* Copyright (C) 2025 Wildfire Games.
/* Copyright (C) 2026 Wildfire Games.
* Copyright (C) 2013-2016 SuperTuxKart-Team.
* This file is part of 0 A.D.
*
@ -46,6 +46,14 @@ bool FindPublicIP(ENetHost& enetClient, CStr8& ip, u16& port);
*/
void SendHolePunchingMessages(ENetHost& enetClient, const std::string& serverAddress, u16 serverPort);
/**
* Send a single hole punching message to the target address.
* Unlike SendHolePunchingMessages this doesn't block, so the caller
* is responsible for repeating the message at a sensible interval.
* @see SendHolePunchingMessages
*/
void SendHolePunchingMessage(ENetHost& enetClient, const ENetAddress& addr);
/**
* Return the local IP.
* Technically not a STUN method, but convenient to define here.