Refactor radar system

This commit is contained in:
Ayzen
2026-10-05 16:46:28 +03:00
parent 515049d2ed
commit 8f097dfa2e
179 changed files with 8212 additions and 6676 deletions
@@ -88,6 +88,7 @@ class ClientSession {
private:
void writer_loop();
void reader_loop(std::atomic<double>& shared_vlc_slot, std::atomic<std::int64_t>& shared_vlc_at_ns);
void finish_loop();
int socket_fd_;
std::string peer_name_;
@@ -148,11 +149,7 @@ class TcpServer {
void acceptor_loop();
void enroll_client(std::unique_ptr<ClientSession> session);
void broadcast_packet(const std::vector<std::uint8_t>& packet);
void reap_finished_clients();
// Best-effort reap from the publish path: acquires clients_mutex_ via try-lock
// (skips if contended) and joins exited sessions outside the lock. Keeps
// clients_ bounded even when no new connections arrive to trigger a reap.
void try_reap_finished_clients();
void reap_finished_clients(bool wait_for_lock = true);
void cache_latest_packet(std::vector<std::uint8_t> packet);
[[nodiscard]] auto latest_packet_copy() const -> std::optional<std::vector<std::uint8_t>>;
@@ -6,7 +6,9 @@
#include <cstring>
#include <ctime>
#include <iomanip>
#include <limits>
#include <sstream>
#include <stdexcept>
#include <string>
#include <nlohmann/json.hpp>
@@ -96,17 +98,6 @@ constexpr std::uint32_t kMinTableColumns = 3U; // [x_m, z_m, score, ...]
return stream.str();
}
// Wall-clock epoch milliseconds at packet-build time. Unlike the human-readable
// `tim` field (which has no date and rolls over at midnight), this is a
// monotonically increasing generation stamp consumers can use to compute the
// true age of a snapshot and detect a stalled pipeline.
[[nodiscard]] auto epoch_millis_now() -> std::int64_t {
using Clock = std::chrono::system_clock;
return std::chrono::duration_cast<std::chrono::milliseconds>(
Clock::now().time_since_epoch()
).count();
}
void append_u32_little_endian(std::vector<std::uint8_t>& buffer, std::uint32_t value) {
buffer.push_back(static_cast<std::uint8_t>(value & 0xFFU));
buffer.push_back(static_cast<std::uint8_t>((value >> 8U) & 0xFFU));
@@ -200,15 +191,6 @@ auto build_payload_json(
{"obs", std::move(obs_array)},
};
// `gen` is a build-time generation stamp (epoch ms) that lets a consumer compute
// a packet's true age and flag a cached/stalled snapshot — something `tim` cannot
// express, having no date and rolling over at midnight. It is currently NOT sent;
// set this flag to true to re-enable it (epoch_millis_now() is kept ready for that).
constexpr bool kEmitGenerationStamp = false;
if (kEmitGenerationStamp) {
root["gen"] = epoch_millis_now();
}
return root.dump();
}
@@ -216,6 +198,9 @@ auto encode_packet(
const std::string& payload_json,
std::uint32_t device_id
) -> std::vector<std::uint8_t> {
if (payload_json.size() > std::numeric_limits<std::uint32_t>::max()) {
throw std::runtime_error("Locator packet payload exceeds uint32 wire limit");
}
const auto payload_size = static_cast<std::uint32_t>(payload_json.size());
std::vector<std::uint8_t> packet;
packet.reserve(static_cast<std::size_t>(8U) + payload_json.size());
@@ -225,10 +225,16 @@ void ClientSession::start(
std::atomic<double>& shared_vlc_slot,
std::atomic<std::int64_t>& shared_vlc_at_ns
) {
writer_thread_ = std::thread([this]() { writer_loop(); });
reader_thread_ = std::thread([this, &shared_vlc_slot, &shared_vlc_at_ns]() {
reader_loop(shared_vlc_slot, shared_vlc_at_ns);
});
try {
writer_thread_ = std::thread([this]() { writer_loop(); });
reader_thread_ = std::thread([this, &shared_vlc_slot, &shared_vlc_at_ns]() {
reader_loop(shared_vlc_slot, shared_vlc_at_ns);
});
} catch (...) {
request_stop();
join();
throw;
}
}
void ClientSession::enqueue(std::vector<std::uint8_t> packet) {
@@ -278,14 +284,7 @@ void ClientSession::writer_loop() {
break;
}
}
request_stop();
// Mark exited only once BOTH loops have finished; otherwise a writer-only
// death (e.g. a send timeout while the reader still blocks on recv) would
// never be reaped. request_stop() above shuts the socket so the reader's
// recv() returns promptly.
if (loops_finished_.fetch_add(1, std::memory_order_acq_rel) + 1U == 2U) {
exited_.store(true, std::memory_order_release);
}
finish_loop();
}
void ClientSession::reader_loop(
@@ -295,51 +294,58 @@ void ClientSession::reader_loop(
std::array<std::uint8_t, kPacketHeaderSize> header_buffer{};
std::vector<std::uint8_t> payload_buffer;
while (!stop_requested_.load(std::memory_order_acquire)) {
if (!read_exact(socket_fd_, header_buffer.data(), header_buffer.size(), stop_requested_)) {
break;
}
const auto payload_len = decode_u32_little_endian(header_buffer.data() + 4U);
if (payload_len > max_payload_bytes_) {
log_warning(
"closing client " + peer_name_ + " after payload size "
+ std::to_string(payload_len) + " exceeded the configured limit"
);
break;
}
try {
while (!stop_requested_.load(std::memory_order_acquire)) {
if (!read_exact(socket_fd_, header_buffer.data(), header_buffer.size(), stop_requested_)) {
break;
}
const auto payload_len = decode_u32_little_endian(header_buffer.data() + 4U);
if (payload_len > max_payload_bytes_) {
log_warning(
"closing client " + peer_name_ + " after payload size "
+ std::to_string(payload_len) + " exceeded the configured limit"
);
break;
}
payload_buffer.assign(payload_len, std::uint8_t{0});
if (payload_len > 0U
&& !read_exact(socket_fd_, payload_buffer.data(), payload_len, stop_requested_)) {
break;
}
payload_buffer.assign(payload_len, std::uint8_t{0});
if (payload_len > 0U
&& !read_exact(socket_fd_, payload_buffer.data(), payload_len, stop_requested_)) {
break;
}
try {
const auto json = Json::parse(payload_buffer.begin(), payload_buffer.end());
if (json.is_object()) {
const auto found = json.find("vlc");
if (found != json.end() && found->is_number()) {
const double value = found->get<double>();
if (std::isfinite(value)) {
// Stamp the value first, then the time, so a reader that
// observes a fresh timestamp also observes the matching value.
shared_vlc_slot.store(value, std::memory_order_release);
const auto now_ns = std::chrono::duration_cast<std::chrono::nanoseconds>(
std::chrono::steady_clock::now().time_since_epoch()
).count();
shared_vlc_at_ns.store(now_ns, std::memory_order_release);
try {
const auto json = Json::parse(payload_buffer.begin(), payload_buffer.end());
if (json.is_object()) {
const auto found = json.find("vlc");
if (found != json.end() && found->is_number()) {
const double value = found->get<double>();
if (std::isfinite(value) && std::fabs(value) <= std::numeric_limits<float>::max()) {
// Stamp the value first, then the time, so a reader that
// observes a fresh timestamp also observes the matching value.
shared_vlc_slot.store(value, std::memory_order_release);
const auto now_ns = std::chrono::duration_cast<std::chrono::nanoseconds>(
std::chrono::steady_clock::now().time_since_epoch()
).count();
shared_vlc_at_ns.store(now_ns, std::memory_order_release);
}
}
}
} catch (const Json::exception& error) {
log_warning(
"ignoring malformed packet from " + peer_name_ + ": " + error.what()
);
}
} catch (const Json::parse_error& error) {
log_warning(
"ignoring malformed packet from " + peer_name_ + ": " + error.what()
);
}
} catch (const std::exception& error) {
log_warning("closing client " + peer_name_ + ": " + error.what());
}
finish_loop();
}
void ClientSession::finish_loop() {
request_stop();
// Mark exited only once BOTH loops have finished (see writer_loop), so a
// reader-only death still waits for the writer before this session is reaped.
// A session is reapable after both threads have stopped using its socket.
if (loops_finished_.fetch_add(1, std::memory_order_acq_rel) + 1U == 2U) {
exited_.store(true, std::memory_order_release);
}
@@ -417,7 +423,13 @@ void TcpServer::start() {
}
listen_fd_ = fd;
acceptor_thread_ = std::thread([this]() { acceptor_loop(); });
try {
acceptor_thread_ = std::thread([this]() { acceptor_loop(); });
} catch (...) {
shutdown_and_close(listen_fd_);
running_.store(false, std::memory_order_release);
throw;
}
}
void TcpServer::stop() {
@@ -427,13 +439,14 @@ void TcpServer::stop() {
if (listen_fd_ >= 0) {
(void)::shutdown(listen_fd_, SHUT_RDWR);
(void)::close(listen_fd_);
listen_fd_ = -1;
}
if (acceptor_thread_.joinable()) {
acceptor_thread_.join();
}
// Keep the descriptor stable until accept() has returned: closing it earlier
// permits fd reuse and races the acceptor's read of listen_fd_.
shutdown_and_close(listen_fd_);
std::vector<std::unique_ptr<ClientSession>> sessions;
{
@@ -466,7 +479,7 @@ void TcpServer::publish(const ipc::ResultCollection& collection, const FilterPar
// Reap exited sessions on the publish path too, so clients_ stays bounded
// even when no new connections arrive to trigger an accept-time reap. Uses a
// try-lock and joins outside the mutex, so publish() never blocks on it.
try_reap_finished_clients();
reap_finished_clients(false);
}
auto TcpServer::latest_socket_speed() const -> std::optional<double> {
@@ -525,19 +538,27 @@ void TcpServer::acceptor_loop() {
reap_finished_clients();
apply_socket_keepalive(client_fd);
auto session = std::make_unique<ClientSession>(
client_fd,
format_peer(peer_addr),
config_.client_queue_size,
config_.max_payload_bytes
);
const auto snapshot = latest_packet_copy();
if (snapshot.has_value()) {
session->enqueue(*snapshot);
std::unique_ptr<ClientSession> session;
bool socket_owned = false;
try {
apply_socket_keepalive(client_fd);
session = std::make_unique<ClientSession>(
client_fd, format_peer(peer_addr), config_.client_queue_size,
config_.max_payload_bytes
);
socket_owned = true;
const auto snapshot = latest_packet_copy();
if (snapshot.has_value()) {
session->enqueue(*snapshot);
}
session->start(latest_socket_speed_, latest_socket_speed_at_ns_);
enroll_client(std::move(session));
} catch (const std::exception& error) {
if (!socket_owned) {
::close(client_fd);
}
log_warning(std::string("cannot start client: ") + error.what());
}
session->start(latest_socket_speed_, latest_socket_speed_at_ns_);
enroll_client(std::move(session));
}
}
@@ -553,35 +574,13 @@ void TcpServer::broadcast_packet(const std::vector<std::uint8_t>& packet) {
}
}
void TcpServer::reap_finished_clients() {
void TcpServer::reap_finished_clients(bool wait_for_lock) {
std::vector<std::unique_ptr<ClientSession>> to_join;
{
std::lock_guard<std::mutex> guard(clients_mutex_);
auto first_dead = std::partition(
clients_.begin(),
clients_.end(),
[](const std::unique_ptr<ClientSession>& session) {
return !session->has_exited();
}
);
for (auto it = first_dead; it != clients_.end(); ++it) {
to_join.push_back(std::move(*it));
}
clients_.erase(first_dead, clients_.end());
}
for (auto& session : to_join) {
session->request_stop();
session->join();
}
}
void TcpServer::try_reap_finished_clients() {
std::vector<std::unique_ptr<ClientSession>> to_join;
{
// try_to_lock: if the acceptor already holds the mutex (it reaps too),
// skip this round rather than block the publish path.
std::unique_lock<std::mutex> guard(clients_mutex_, std::try_to_lock);
if (!guard.owns_lock()) {
std::unique_lock<std::mutex> guard(clients_mutex_, std::defer_lock);
if (wait_for_lock) {
guard.lock();
} else if (!guard.try_lock()) {
return;
}
auto first_dead = std::partition(