TCP: Implemented ACK return stream, as a feedback channel (to be read properly!)

This commit is contained in:
2026-03-04 12:24:05 +01:00
parent 939bb02ce2
commit a3a986830b
13 changed files with 611 additions and 80 deletions

View File

@@ -197,6 +197,50 @@ bool TCPStreamPusherSocket::SendAll(const void *buf, size_t len) {
return true;
}
bool TCPStreamPusherSocket::ReadExact(void *buf, size_t len) {
auto *p = static_cast<uint8_t *>(buf);
size_t got = 0;
while (got < len) {
if (!active)
return false;
int local_fd = fd.load();
if (local_fd < 0)
return false;
pollfd pfd{};
pfd.fd = local_fd;
pfd.events = POLLIN;
const int prc = poll(&pfd, 1, 100); // 100 ms interruptibility window
if (prc == 0)
continue;
if (prc < 0) {
if (errno == EINTR)
continue;
return false;
}
if ((pfd.revents & (POLLERR | POLLHUP | POLLNVAL)) != 0)
return false;
if ((pfd.revents & POLLIN) == 0)
continue;
ssize_t rc = ::recv(local_fd, p + got, len - got, 0);
if (rc == 0)
return false;
if (rc < 0) {
if (errno == EINTR)
continue;
if (errno == EAGAIN || errno == EWOULDBLOCK)
continue;
return false;
}
got += static_cast<size_t>(rc);
}
return true;
}
bool TCPStreamPusherSocket::SendPayloadZC(const uint8_t *data, size_t size, ZeroCopyReturnValue *z) {
#if defined(MSG_ZEROCOPY) && defined(SO_ZEROCOPY)
int local_fd = fd.load();
@@ -358,10 +402,89 @@ void TCPStreamPusherSocket::CompletionThread() {
#endif
}
void TCPStreamPusherSocket::AckThread() {
while (active) {
TcpFrameHeader h{};
if (!ReadExact(&h, sizeof(h))) {
if (active) {
broken = true;
logger.Error("TCP ACK reader disconnected on " + endpoint);
}
break;
}
if (h.magic != JFJOCH_TCP_MAGIC || h.version != JFJOCH_TCP_VERSION || static_cast<TCPFrameType>(h.type) != TCPFrameType::ACK) {
broken = true;
logger.Error("Invalid ACK frame on " + endpoint);
break;
}
std::string error_text;
if (h.payload_size > 0) {
error_text.resize(h.payload_size);
if (!ReadExact(error_text.data(), error_text.size())) {
broken = true;
break;
}
}
const auto ack_for = static_cast<TCPFrameType>(h.ack_for);
const bool ok = (h.flags & TCP_ACK_FLAG_OK) != 0;
const bool fatal = (h.flags & TCP_ACK_FLAG_FATAL) != 0;
const auto code = static_cast<TCPAckCode>(h.ack_code);
{
std::unique_lock ul(ack_state_mutex);
last_ack_code = code;
if (!error_text.empty())
last_ack_error = error_text;
if (ack_for == TCPFrameType::START) {
start_ack_received = true;
start_ack_ok = ok;
if (!ok && error_text.empty())
last_ack_error = "START rejected";
} else if (ack_for == TCPFrameType::END) {
end_ack_received = true;
end_ack_ok = ok;
if (!ok && error_text.empty())
last_ack_error = "END rejected";
} else if (ack_for == TCPFrameType::CANCEL) {
cancel_ack_received = true;
cancel_ack_ok = ok;
if (!ok && error_text.empty())
last_ack_error = "CANCEL rejected";
} else if (ack_for == TCPFrameType::DATA && (!ok || fatal)) {
broken = true;
if (error_text.empty())
last_ack_error = "DATA fatal ACK";
logger.Error("Received fatal DATA ACK on " + endpoint + ": " + last_ack_error);
}
}
ack_cv.notify_all();
}
}
void TCPStreamPusherSocket::StartWriterThread() {
if (active)
return;
{
std::unique_lock ul(ack_state_mutex);
start_ack_received = false;
start_ack_ok = false;
end_ack_received = false;
end_ack_ok = false;
cancel_ack_received = false;
cancel_ack_ok = false;
last_ack_error.clear();
last_ack_code = TCPAckCode::None;
}
active = true;
send_future = std::async(std::launch::async, &TCPStreamPusherSocket::WriterThread, this);
completion_future = std::async(std::launch::async, &TCPStreamPusherSocket::CompletionThread, this);
ack_future = std::async(std::launch::async, &TCPStreamPusherSocket::AckThread, this);
}
void TCPStreamPusherSocket::StopWriterThread() {
@@ -369,11 +492,14 @@ void TCPStreamPusherSocket::StopWriterThread() {
return;
active = false;
queue.PutBlocking({.end = true});
ack_cv.notify_all();
if (send_future.valid())
send_future.get();
if (completion_future.valid())
completion_future.get();
if (ack_future.valid())
ack_future.get();
// Keep fd open: END frame may still be sent after writer thread stops.
// Socket is closed in destructor / explicit close path.
@@ -403,3 +529,46 @@ bool TCPStreamPusherSocket::Send(const uint8_t *data, size_t size, TCPFrameType
return SendFrame(data, size, type, image_number, nullptr);
}
bool TCPStreamPusherSocket::WaitForAck(TCPFrameType ack_for, std::chrono::milliseconds timeout, std::string *error_text) {
std::unique_lock ul(ack_state_mutex);
const bool ok = ack_cv.wait_for(ul, timeout, [&] {
if (ack_for == TCPFrameType::START)
return start_ack_received || broken.load();
if (ack_for == TCPFrameType::END)
return end_ack_received || broken.load();
if (ack_for == TCPFrameType::CANCEL)
return cancel_ack_received || broken.load();
return false;
});
if (!ok) {
if (error_text)
*error_text = "ACK timeout";
return false;
}
if (broken) {
if (error_text)
*error_text = last_ack_error.empty() ? "Socket broken" : last_ack_error;
return false;
}
bool ack_ok = false;
if (ack_for == TCPFrameType::START)
ack_ok = start_ack_ok;
else if (ack_for == TCPFrameType::END)
ack_ok = end_ack_ok;
else if (ack_for == TCPFrameType::CANCEL)
ack_ok = cancel_ack_ok;
if (!ack_ok && error_text)
*error_text = last_ack_error.empty() ? "ACK rejected" : last_ack_error;
return ack_ok;
}
std::string TCPStreamPusherSocket::GetLastAckError() const {
std::unique_lock ul(ack_state_mutex);
return last_ack_error;
}