Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added timeout to send for tcp_client and tcp_client-windows #3355

Open
wants to merge 3 commits into
base: v1.x
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 3 additions & 10 deletions example/example.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ void multi_sink_example() {
struct my_type {
int i = 0;
explicit my_type(int i)
: i(i){}
: i(i) {}
};

#ifndef SPDLOG_USE_STD_FORMAT // when using fmtlib
Expand Down Expand Up @@ -378,18 +378,11 @@ void replace_default_logger_example() {
spdlog::debug("This message should not be displayed!");
spdlog::set_level(spdlog::level::trace);
spdlog::debug("This message should be displayed..");

spdlog::set_default_logger(old_logger);
}

// Mapped Diagnostic Context (MDC) is a map that stores key-value pairs (string values) in thread local storage.
// Each thread maintains its own MDC, which loggers use to append diagnostic information to log outputs.
// Note: it is not supported in asynchronous mode due to its reliance on thread-local storage.

#ifndef SPDLOG_NO_TLS
#include "spdlog/mdc.h"
void mdc_example()
{
void mdc_example() {
spdlog::mdc::put("key1", "value1");
spdlog::mdc::put("key2", "value2");
// if not using the default format, you can use the %& formatter to print mdc data as well
Expand All @@ -400,4 +393,4 @@ void mdc_example()
void mdc_example() {
// if TLS feature is disabled
}
#endif
#endif
78 changes: 46 additions & 32 deletions include/spdlog/details/tcp_client-windows.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

namespace spdlog {
namespace details {

class tcp_client {
SOCKET socket_ = INVALID_SOCKET;

Expand All @@ -37,42 +38,24 @@ class tcp_client {
::FormatMessageA(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, NULL,
last_error, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), buf,
(sizeof(buf) / sizeof(char)), NULL);

throw_spdlog_ex(fmt_lib::format("tcp_sink - {}: {}", msg, buf));
}

public:
tcp_client() { init_winsock_(); }

~tcp_client() {
close();
::WSACleanup();
}

bool is_connected() const { return socket_ != INVALID_SOCKET; }

void close() {
::closesocket(socket_);
socket_ = INVALID_SOCKET;
}

SOCKET fd() const { return socket_; }

// try to connect or throw on failure
void connect(const std::string &host, int port) {
void connect(const std::string &host, int port, int timeout_sec) {
if (is_connected()) {
close();
}
struct addrinfo hints {};
struct addrinfo hints{};
ZeroMemory(&hints, sizeof(hints));

hints.ai_family = AF_UNSPEC; // To work with IPv4, IPv6, and so on
hints.ai_family = AF_UNSPEC; // IPv4 or IPv6
hints.ai_socktype = SOCK_STREAM; // TCP
hints.ai_flags = AI_NUMERICSERV; // port passed as as numeric value
hints.ai_flags = AI_NUMERICSERV; // port as numeric value
hints.ai_protocol = 0;
int timeout_ms = timeout_sec * 1000;

auto port_str = std::to_string(port);
struct addrinfo *addrinfo_result;
struct addrinfo *addrinfo_result = nullptr;
auto rv = ::getaddrinfo(host.c_str(), port_str.c_str(), &hints, &addrinfo_result);
int last_error = 0;
if (rv != 0) {
Expand All @@ -81,13 +64,11 @@ class tcp_client {
throw_winsock_error_("getaddrinfo failed", last_error);
}

// Try each address until we successfully connect(2).

// Try each address until we successfully connect.
for (auto *rp = addrinfo_result; rp != nullptr; rp = rp->ai_next) {
socket_ = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
if (socket_ == INVALID_SOCKET) {
last_error = ::WSAGetLastError();
WSACleanup();
continue;
}
if (::connect(socket_, rp->ai_addr, (int)rp->ai_addrlen) == 0) {
Expand All @@ -103,33 +84,66 @@ class tcp_client {
throw_winsock_error_("connect failed", last_error);
}

if (::setsockopt(socket_, SOL_SOCKET, SO_RCVTIMEO,
reinterpret_cast<const char *>(&timeout_ms),
sizeof(timeout_ms)) == SOCKET_ERROR) {
last_error = ::WSAGetLastError();
WSACleanup();
throw_winsock_error_("setsockopt(SO_RCVTIMEO) failed", last_error);
}
if (::setsockopt(socket_, SOL_SOCKET, SO_SNDTIMEO,
reinterpret_cast<const char *>(&timeout_ms),
sizeof(timeout_ms)) == SOCKET_ERROR) {
last_error = ::WSAGetLastError();
WSACleanup();
throw_winsock_error_("setsockopt(SO_SNDTIMEO) failed", last_error);
}

// set TCP_NODELAY
int enable_flag = 1;
::setsockopt(socket_, IPPROTO_TCP, TCP_NODELAY, reinterpret_cast<char *>(&enable_flag),
sizeof(enable_flag));
}

bool is_connected() const { return socket_ != INVALID_SOCKET; }

void close() {
if (is_connected()) {
::closesocket(socket_);
socket_ = INVALID_SOCKET;
}
}

SOCKET fd() const { return socket_; }

~tcp_client() {
close();
::WSACleanup();
}

// Send exactly n_bytes of the given data.
// On error close the connection and throw.
void send(const char *data, size_t n_bytes) {
size_t bytes_sent = 0;
while (bytes_sent < n_bytes) {
const int send_flags = 0;
auto write_result =
::send(socket_, data + bytes_sent, (int)(n_bytes - bytes_sent), send_flags);
auto write_result = ::send(socket_, data + bytes_sent,
static_cast<int>(n_bytes - bytes_sent), send_flags);
if (write_result == SOCKET_ERROR) {
int last_error = ::WSAGetLastError();
close();
if (last_error == WSAETIMEDOUT) {
throw_winsock_error_("Connection timed out", last_error);
}
throw_winsock_error_("send failed", last_error);
}

if (write_result == 0) // (probably should not happen but in any case..)
{
if (write_result == 0) { // (probably should not happen but in any case..)
break;
}
bytes_sent += static_cast<size_t>(write_result);
}
}
};

} // namespace details
} // namespace spdlog
19 changes: 15 additions & 4 deletions include/spdlog/details/tcp_client.h
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,9 @@ class tcp_client {
~tcp_client() { close(); }

// try to connect or throw on failure
void connect(const std::string &host, int port) {
void connect(const std::string &host, int port, int timeout_sec) {
close();
struct addrinfo hints {};
struct addrinfo hints{};
memset(&hints, 0, sizeof(struct addrinfo));
hints.ai_family = AF_UNSPEC; // To work with IPv4, IPv6, and so on
hints.ai_socktype = SOCK_STREAM; // TCP
Expand All @@ -56,7 +56,6 @@ class tcp_client {
throw_spdlog_ex(fmt_lib::format("::getaddrinfo failed: {}", gai_strerror(rv)));
}

// Try each address until we successfully connect(2).
int last_errno = 0;
for (auto *rp = addrinfo_result; rp != nullptr; rp = rp->ai_next) {
#if defined(SOCK_CLOEXEC)
Expand All @@ -69,6 +68,14 @@ class tcp_client {
last_errno = errno;
continue;
}
struct timeval timeout;
timeout.tv_sec = timeout_sec;
timeout.tv_usec = 0;
::setsockopt(socket_, SOL_SOCKET, SO_RCVTIMEO, reinterpret_cast<char *>(&timeout),
sizeof(timeout));
::setsockopt(socket_, SOL_SOCKET, SO_SNDTIMEO, reinterpret_cast<char *>(&timeout),
sizeof(timeout));
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i dont think it affects connect timeout. only recv and send

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes it only send and recv timeout, I can do it for connect but it would be non blocking I guess. Can I proceed with that ?

Copy link
Owner

@gabime gabime Mar 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am confused. Isn't the connect timeout is the whole point of this pr?


rv = ::connect(socket_, rp->ai_addr, rp->ai_addrlen);
if (rv == 0) {
break;
Expand All @@ -83,7 +90,7 @@ class tcp_client {
}

// set TCP_NODELAY
int enable_flag = 1;
int enable_flag = 1;
::setsockopt(socket_, IPPROTO_TCP, TCP_NODELAY, reinterpret_cast<char *>(&enable_flag),
sizeof(enable_flag));

Expand Down Expand Up @@ -111,7 +118,11 @@ class tcp_client {
auto write_result =
::send(socket_, data + bytes_sent, n_bytes - bytes_sent, send_flags);
if (write_result < 0) {
int err = errno;
close();
if (err == ETIMEDOUT) {
throw_spdlog_ex("Connection timed out", ETIMEDOUT);
}
throw_spdlog_ex("write(2) failed", errno);
}

Expand Down
13 changes: 8 additions & 5 deletions include/spdlog/sinks/tcp_sink.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,13 @@ namespace sinks {
struct tcp_sink_config {
std::string server_host;
int server_port;
int timeout_sec;
bool lazy_connect = false; // if true connect on first log call instead of on construction

tcp_sink_config(std::string host, int port)
tcp_sink_config(std::string host, int port, int timeout_sec)
: server_host{std::move(host)},
server_port{port} {}
server_port{port},
timeout_sec(timeout_sec) {}
};

template <typename Mutex>
Expand All @@ -47,19 +49,20 @@ class tcp_sink : public spdlog::sinks::base_sink<Mutex> {
explicit tcp_sink(tcp_sink_config sink_config)
: config_{std::move(sink_config)} {
if (!config_.lazy_connect) {
this->client_.connect(config_.server_host, config_.server_port);
this->client_.connect(config_.server_host, config_.server_port, config_.timeout_sec);
}
}

~tcp_sink() override = default;

protected:
void sink_it_(const spdlog::details::log_msg &msg) override {
void sink_it_(const spdlog::details::log_msg& msg) override {
spdlog::memory_buf_t formatted;
spdlog::sinks::base_sink<Mutex>::formatter_->format(msg, formatted);
if (!client_.is_connected()) {
client_.connect(config_.server_host, config_.server_port);
client_.connect(config_.server_host, config_.server_port, config_.timeout_sec);
}

client_.send(formatted.data(), formatted.size());
}

Expand Down