ascii-chat
Binary v0.15.83-53-gb711d8720
Library lib/v0.3.2-701-gb711d8720
Video chat in your terminal
Loading...
Searching...
No Matches
parallel_connect.h File Reference

Parallel IPv4/IPv6 connection with race-to-connect semantics. More...

Go to the source code of this file.

Data Structures

struct  parallel_connect_config_t

Typedefs

typedef bool(* parallel_connect_should_exit_fn) (void *user_data)
 When code needs to work with parallel connect config, it keeps hostname, port, timeout_ms, should_exit_callback here instead of scattering it across globals. Initialization establishes the handles and sizes; teardown must release any allocation, lock, thread, or callback context recorded in those fields.

Functions

asciichat_error_t parallel_connect (const parallel_connect_config_t *config, socket_t *out_socket)

Detailed Description

Parallel IPv4/IPv6 connection with race-to-connect semantics.

Attempts IPv4 and IPv6 connections in parallel and returns the first successful socket, closing the loser. Handles unreachable addresses gracefully without blocking on timeouts.

Definition in file parallel_connect.h.

Typedef Documentation

◆ parallel_connect_should_exit_fn

typedef bool(* parallel_connect_should_exit_fn) (void *user_data)

When code needs to work with parallel connect config, it keeps hostname, port, timeout_ms, should_exit_callback here instead of scattering it across globals. Initialization establishes the handles and sizes; teardown must release any allocation, lock, thread, or callback context recorded in those fields.

Callback type to check if connection should be abandoned

Called periodically by connection threads to allow graceful shutdown. Should return true if connection attempts should stop immediately.

Parameters
user_dataOpaque pointer provided by caller
Returns
true to abandon connection attempts, false to continue

Definition at line 27 of file parallel_connect.h.

Function Documentation

◆ parallel_connect()

asciichat_error_t parallel_connect ( const parallel_connect_config_t * config,
socket_t * out_socket )

Connect to hostname with parallel IPv4/IPv6 attempts

Resolves hostname and attempts IPv4 and IPv6 connections in parallel. Returns the first successful socket; the losing connection is closed. Each connection attempt is non-blocking with select() timeout.

Parameters
configConnection configuration
out_socketPointer to receive winning socket
Returns
ASCIICHAT_OK on success, ERROR_* on failure
Note
Caller is responsible for closing the returned socket
Timeout applies to each connection attempt independently

Definition at line 182 of file parallel_connect.c.

182 {
183 ASCII_OTEL_SCOPE(PARALLEL_CONNECT);
184 if (!config || !out_socket) {
185 return SET_ERRNO(ERROR_INVALID_PARAM, "config or out_socket is NULL");
186 }
187
188 *out_socket = INVALID_SOCKET_VALUE;
189
190 char ascii_hostname[HOSTNAME_BUFFER_SIZE];
191 asciichat_error_t normalized = hostname_to_ascii(config->hostname, ascii_hostname, sizeof(ascii_hostname));
192 if (normalized != ASCIICHAT_OK)
193 return normalized;
194
195 // Resolve hostname
196 log_debug("PCONN: Resolving %s:%d", config->hostname, config->port);
197
198 struct addrinfo hints;
199 struct addrinfo *result = NULL;
200 memset(&hints, 0, sizeof(hints));
201 hints.ai_family = AF_UNSPEC;
202 hints.ai_socktype = SOCK_STREAM;
203
205 safe_snprintf(port_str, sizeof(port_str), "%d", config->port);
206
207 int gai_result;
208 {
209 ASCII_OTEL_SCOPE(DNS_RESOLVE);
210 if (otel_scope.retained)
211 gotel_attr_string(&otel_scope.scope, "dns.question.name", config->hostname);
212 gai_result = getaddrinfo(ascii_hostname, port_str, &hints, &result);
213 if (gai_result && otel_scope.retained)
214 gotel_error(&otel_scope.scope, "dns.error", "DNS lookup failed");
215 }
216 if (gai_result != 0) {
217 return SET_ERRNO(ERROR_NETWORK, "Failed to resolve %s: %s", config->hostname, gai_strerror(gai_result));
218 }
219
220 // Find first IPv4 and IPv6 addresses
221 struct addrinfo *ipv4_addr = NULL;
222 struct addrinfo *ipv6_addr = NULL;
223
224 for (struct addrinfo *rp = result; rp != NULL; rp = rp->ai_next) {
225 if (rp->ai_family == AF_INET && !ipv4_addr) {
226 ipv4_addr = rp;
227 log_debug("PCONN: Found IPv4 address");
228 } else if (rp->ai_family == AF_INET6 && !ipv6_addr) {
229 ipv6_addr = rp;
230 log_debug("PCONN: Found IPv6 address");
231 }
232 if (ipv4_addr && ipv6_addr)
233 break;
234 }
235
236 // Need at least one address
237 if (!ipv4_addr && !ipv6_addr) {
238 freeaddrinfo(result);
239 return SET_ERRNO(ERROR_NETWORK, "No IPv4 or IPv6 addresses found for %s", config->hostname);
240 }
241
242 // Shared state
243 mutex_t lock;
244 cond_t signal;
245 mutex_init(&lock, "parallel_connect");
246 cond_init(&signal, "parallel_connect");
247
248 volatile socket_t winner_socket = INVALID_SOCKET_VALUE;
249 volatile bool winner_found = false;
250
251 // An unavailable address family never starts a worker. Its cleanup must not
252 // close descriptor zero, which can subsequently be reused by the event loop.
253 connection_attempt_t ipv4_attempt = {.socket = INVALID_SOCKET_VALUE};
254 connection_attempt_t ipv6_attempt = {.socket = INVALID_SOCKET_VALUE};
255 platform_thread_t ipv4_thread;
256 platform_thread_t ipv6_thread;
257
258 thread_init(&ipv4_thread);
259 thread_init(&ipv6_thread);
260
261 // Start IPv4 attempt if available
262 if (ipv4_addr) {
263 ipv4_attempt.family = AF_INET;
264 ipv4_attempt.family_name = "IPv4";
265 ipv4_attempt.timeout_ms = config->timeout_ms;
266 ipv4_attempt.winner_socket = &winner_socket;
267 ipv4_attempt.winner_found = &winner_found;
268 ipv4_attempt.lock = &lock;
269 ipv4_attempt.signal = &signal;
270 ipv4_attempt.should_exit_callback = config->should_exit_callback;
271 ipv4_attempt.callback_data = config->callback_data;
272 memcpy(&ipv4_attempt.addr, ipv4_addr->ai_addr, ipv4_addr->ai_addrlen);
273 ipv4_attempt.addr_len = ipv4_addr->ai_addrlen;
274
275 platform_thread_create(&ipv4_thread, "connect_ipv4", attempt_connection_thread, &ipv4_attempt);
276 }
277
278 // Start IPv6 attempt if available
279 if (ipv6_addr) {
280 ipv6_attempt.family = AF_INET6;
281 ipv6_attempt.family_name = "IPv6";
282 ipv6_attempt.timeout_ms = config->timeout_ms;
283 ipv6_attempt.winner_socket = &winner_socket;
284 ipv6_attempt.winner_found = &winner_found;
285 ipv6_attempt.lock = &lock;
286 ipv6_attempt.signal = &signal;
287 ipv6_attempt.should_exit_callback = config->should_exit_callback;
288 ipv6_attempt.callback_data = config->callback_data;
289 memcpy(&ipv6_attempt.addr, ipv6_addr->ai_addr, ipv6_addr->ai_addrlen);
290 ipv6_attempt.addr_len = ipv6_addr->ai_addrlen;
291
292 platform_thread_create(&ipv6_thread, "connect_ipv6", attempt_connection_thread, &ipv6_attempt);
293 }
294
295 // Wait for winner or both to finish
296 uint32_t max_wait_ms = config->timeout_ms + 1000;
297 uint32_t elapsed_ms = 0;
298
299 mutex_lock(&lock);
300 while (!winner_found && elapsed_ms < max_wait_ms) {
301 cond_timedwait(&signal, &lock, 100 * NS_PER_MS_INT);
302 elapsed_ms += 100;
303
304 // Check if both are done
305 if (ipv4_addr && ipv6_addr) {
306 if (ipv4_attempt.done && ipv6_attempt.done) {
307 break;
308 }
309 } else if ((ipv4_addr && ipv4_attempt.done) || (ipv6_addr && ipv6_attempt.done)) {
310 break;
311 }
312 }
313 mutex_unlock(&lock);
314
315 // Join threads (must wait for both to complete before returning)
316 // The threads access shared state on our stack, so we cannot return until they're done
317 if (thread_is_initialized(&ipv4_thread)) {
318 thread_join(&ipv4_thread, NULL);
319 }
320 if (thread_is_initialized(&ipv6_thread)) {
321 thread_join(&ipv6_thread, NULL);
322 }
323
324 // Close any remaining sockets
325 if (ipv4_attempt.socket != INVALID_SOCKET_VALUE) {
326 socket_close(ipv4_attempt.socket);
327 }
328 if (ipv6_attempt.socket != INVALID_SOCKET_VALUE) {
329 socket_close(ipv6_attempt.socket);
330 }
331
332 // Cleanup
333 mutex_destroy(&lock);
334 cond_destroy(&signal);
335 freeaddrinfo(result);
336
337 // Check result
338 if (winner_socket == INVALID_SOCKET_VALUE) {
339 return SET_ERRNO(ERROR_NETWORK, "Failed to connect to %s:%d (both IPv4 and IPv6 failed)", config->hostname,
340 config->port);
341 }
342
343 *out_socket = (socket_t)winner_socket;
344 log_info("PCONN: Successfully connected to %s:%d", config->hostname, config->port);
345 return ASCIICHAT_OK;
346}
#define SET_ERRNO(code,...)
unsigned int uint32_t
Definition common.h:62
asciichat_error_t
Error and exit codes - unified status values (0-255).
Definition error_codes.h:44
@ ERROR_NETWORK
Definition error_codes.h:72
@ ASCIICHAT_OK
Definition error_codes.h:46
@ ERROR_INVALID_PARAM
#define HOSTNAME_BUFFER_SIZE
#define log_info(...)
Log an INFO message.
Definition log/log.h:608
#define log_debug(...)
Log a DEBUG message.
Definition log/log.h:595
#define NS_PER_MS_INT
Definition time.h:168
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Platform-safe snprintf wrapper.
asciichat_error_t hostname_to_ascii(const char *hostname, char *output, size_t capacity)
Definition url.c:103
int socket_t
#define ASCII_OTEL_SCOPE(id)
Definition otel.h:72
#define NETWORK_PARALLEL_CONNECT_PORT_BUFFER_SIZE
Per-thread connection attempt state for parallel IPv4/IPv6 racing.
volatile bool done
True if this attempt finished.
volatile bool * winner_found
Shared flag indicating a winner exists.
int family
Address family (AF_INET or AF_INET6).
socklen_t addr_len
Length of address structure.
void * callback_data
User data for callback.
mutex_t * lock
Shared mutex for coordination.
parallel_connect_should_exit_fn should_exit_callback
Optional exit callback for graceful shutdown.
const char * family_name
Human-readable family name ("IPv4"/"IPv6").
uint32_t timeout_ms
Connection timeout in milliseconds.
socket_t socket
Socket for this attempt.
cond_t * signal
Shared condition variable for signaling.
struct sockaddr_storage addr
Socket address to connect to.
volatile socket_t * winner_socket
Shared pointer to winning socket.
parallel_connect_should_exit_fn should_exit_callback

References connection_attempt_t::addr, connection_attempt_t::addr_len, ASCII_OTEL_SCOPE, ASCIICHAT_OK, connection_attempt_t::callback_data, parallel_connect_config_t::callback_data, connection_attempt_t::done, ERROR_INVALID_PARAM, ERROR_NETWORK, connection_attempt_t::family, connection_attempt_t::family_name, parallel_connect_config_t::hostname, HOSTNAME_BUFFER_SIZE, hostname_to_ascii(), connection_attempt_t::lock, log_debug, log_info, NETWORK_PARALLEL_CONNECT_PORT_BUFFER_SIZE, NS_PER_MS_INT, parallel_connect_config_t::port, safe_snprintf(), SET_ERRNO, connection_attempt_t::should_exit_callback, parallel_connect_config_t::should_exit_callback, connection_attempt_t::signal, connection_attempt_t::socket, connection_attempt_t::timeout_ms, parallel_connect_config_t::timeout_ms, connection_attempt_t::winner_found, and connection_attempt_t::winner_socket.