ascii-chat
Binary v0.15.83-53-gb711d8720
Library lib/v0.3.2-701-gb711d8720
Video chat in your terminal
Loading...
Searching...
No Matches
backpressure.c
Go to the documentation of this file.
2#include <ascii-chat/otel.h>
6#include <libplatform/cond.h>
7#include <libplatform/thread.h>
8#include <ascii-chat/buffer.h>
9#include <ascii-chat/common.h>
12#include <string.h>
13
14#define OUTBOUND_MAX_PACKETS 512
15#define OUTBOUND_MAX_STREAMS 64
16#define WIRE_OVERHEAD (sizeof(packet_header_t) + 32 + CRYPTO_NONCE_SIZE + CRYPTO_MAC_SIZE)
17
32
49
68
69static size_t backend_bytes(acip_outbound_t *out) {
70 return out->transport->methods->queued_bytes ? out->transport->methods->queued_bytes(out->transport) : 0;
71}
72
73static outbound_stream_t *stream_state(acip_outbound_t *out, uint16_t type, uint32_t id) {
74 size_t stream_count = (size_t)atomic_load_u64(&out->stream_count);
75 for (size_t i = 0; i < stream_count; ++i)
76 if (out->streams[i].type == type && out->streams[i].stream == id) {
77 out->streams[i].last_used_ns = time_get_ns();
78 return &out->streams[i];
79 }
80 if (stream_count == OUTBOUND_MAX_STREAMS) {
81 size_t victim = OUTBOUND_MAX_STREAMS;
82 for (size_t i = 0; i < stream_count; ++i) {
83 bool pending = false;
84 for (outbound_packet_t *p = out->head; p; p = p->next)
85 if (p->encoded && p->type == out->streams[i].type && p->stream == out->streams[i].stream) {
86 pending = true;
87 break;
88 }
89 if (!pending &&
90 (victim == OUTBOUND_MAX_STREAMS || out->streams[i].last_used_ns < out->streams[victim].last_used_ns))
91 victim = i;
92 }
93 if (victim == OUTBOUND_MAX_STREAMS) {
94 log_warn("Outbound stream state limit reached (%u); all stream slots are active", OUTBOUND_MAX_STREAMS);
95 return NULL;
96 }
97 log_warn("Evicting inactive outbound stream state for type %u stream %u", out->streams[victim].type,
98 out->streams[victim].stream);
99 outbound_stream_t *stream = &out->streams[victim];
100 *stream = (outbound_stream_t){.type = type, .stream = id, .last_used_ns = time_get_ns()};
101 atomic_store_bool(&stream->needs_keyframe, true);
102 return stream;
103 }
104 size_t index = stream_count;
105 outbound_stream_t *stream = &out->streams[index];
106 *stream = (outbound_stream_t){.type = type, .stream = id, .last_used_ns = time_get_ns()};
107 atomic_store_bool(&stream->needs_keyframe, true);
108 // Publish immutable identity only after initialization. The keyframe bit is
109 // atomic because it is updated independently of the stream identity.
110 atomic_store_u64(&out->stream_count, index + 1);
111 return stream;
112}
113
114// Keep a small connection-owned working set instead of visiting the global
115// allocator and buffer domain for every media packet. All access holds mutex.
116static void packet_free(acip_outbound_t *out, outbound_packet_t *packet) {
117 if (!out->stopping && !out->failed && out->recycled_count < 8 && buffer_capacity(packet->data) <= 256 * 1024) {
118 packet->next = out->recycled;
119 out->recycled = packet;
120 ++out->recycled_count;
121 return;
122 }
123 buffer_release(packet->data);
124 SAFE_FREE(packet);
125}
126
127static bool same_stream(const outbound_packet_t *a, const outbound_packet_t *b) {
128 return a->type == b->type && a->stream == b->stream;
129}
130
131static bool outbound_insert_packet(acip_outbound_t *out, outbound_packet_t *packet, bool priority) {
132 outbound_packet_t **link = &out->head;
133 if (priority) {
134 for (outbound_packet_t **scan = &out->head; *scan; scan = &(*scan)->next)
135 if (!(*scan)->video && !(*scan)->audio && (*scan)->type != PACKET_TYPE_PING && (*scan)->type != PACKET_TYPE_PONG)
136 link = &(*scan)->next;
137 }
138 while (*link && (!priority || !(*link)->video))
139 link = &(*link)->next;
140 bool was_empty = out->head == NULL;
141 packet->next = *link;
142 *link = packet;
143 return was_empty;
144}
145
146/* Conservatively invalidate all unsent encoded output for this stream. The
147 * producer requests an IDR with parameter sets before resuming its chain. */
148static void discard_stream(acip_outbound_t *out, const outbound_packet_t *video) {
149 if (video->encoded) {
150 outbound_stream_t *stream = stream_state(out, video->type, video->stream);
151 if (stream)
152 atomic_store_bool(&stream->needs_keyframe, true);
153 }
154 outbound_packet_t **link = &out->head;
155 while (*link) {
156 outbound_packet_t *packet = *link;
157 if (packet->video && same_stream(packet, video)) {
158 *link = packet->next;
159 out->bytes -= packet->cost;
160 --out->count;
161 atomic_fetch_add_u64(&out->dropped, 1);
162 ascii_otel_metric(ASCII_OTEL_METRIC_OUTBOUND_DROP, 1);
163 packet_free(out, packet);
164 } else {
165 link = &packet->next;
166 }
167 }
168}
169
170static void expire_video(acip_outbound_t *out, uint64_t now) {
171 for (;;) {
172 outbound_packet_t *expired = NULL;
173 for (outbound_packet_t *p = out->head; p; p = p->next)
174 if (p->video && now - p->created > ACIP_OUTBOUND_VIDEO_AGE_NS) {
175 expired = p;
176 break;
177 }
178 if (!expired)
179 break;
180 outbound_packet_t identity = *expired;
181 discard_stream(out, &identity);
182 }
183}
184
185static void expire_datagrams(acip_outbound_t *out, uint64_t now) {
186 outbound_packet_t **link = &out->head;
187 while (*link) {
188 outbound_packet_t *packet = *link;
189 if (packet->options.delivery != ACIP_DELIVERY_DATAGRAM || !packet->options.deadline_ns ||
190 packet->options.deadline_ns > now) {
191 link = &packet->next;
192 continue;
193 }
194 if (packet->encoded) {
195 outbound_packet_t identity = *packet;
196 discard_stream(out, &identity);
197 link = &out->head;
198 continue;
199 }
200 *link = packet->next;
201 out->bytes -= packet->cost;
202 --out->count;
203 atomic_fetch_add_u64(&out->dropped, 1);
204 ascii_otel_metric(ASCII_OTEL_METRIC_OUTBOUND_DROP, 1);
205 packet_free(out, packet);
206 }
207}
208
209// Keep the terminal wrapper plus its relevant nested cause. Follow causal IDs,
210// not merely older registry entries, and never cross this send's checkpoint.
211static asciichat_error_context_t backend_failure_detail(asciichat_errno_scope_t scope,
212 const asciichat_error_context_t *cause) {
214 size_t count = errno_snapshot(scope.thread_id, frames, ASCIICHAT_ERRNO_MAX_DEPTH).copied;
215 asciichat_error_context_t detail = {0};
216 uint64_t next = cause->cause_id;
217 for (size_t i = 0; i < count && next; ++i) {
218 if (frames[i].generation != scope.generation || frames[i].error_id <= scope.after_id)
219 continue;
220 if (frames[i].error_id != next)
221 continue;
222 if (!detail.has_system_error && !detail.has_wsa_error)
223 detail = frames[i];
224 else if (frames[i].has_system_error || frames[i].has_wsa_error)
225 detail = frames[i];
226 next = frames[i].cause_id;
227 }
228 return detail;
229}
230
231static asciichat_error_t import_failure(const asciichat_error_context_t *cause,
232 const asciichat_error_context_t *detail) {
233 if (detail->code != ASCIICHAT_OK && !cause->has_system_error && !cause->has_wsa_error) {
234 errno_import(detail);
235 // Recreate the wrapper at its original source location. Normal recording
236 // links it to the newly imported cause in this producer's registry; simply
237 // importing both would leave the wrapper pointing at the handled worker ID.
238 return errno_set_with_message(cause->code, cause->file, cause->line, cause->function, "%s", cause->context_message);
239 }
240 return errno_import(cause);
241}
242
243// The caller holds mutex. Retain the first terminal cause, reject later
244// admissions, and wake queued producers with that failure. Backend close is
245// performed by the worker after releasing mutex.
246static void outbound_record_failure_and_fail_pending(acip_outbound_t *out, asciichat_error_t code,
247 const asciichat_error_context_t *cause,
248 const asciichat_error_context_t *detail) {
249 if (!out->failed) {
250 out->failure_code = code;
251 if (cause)
252 out->failure = *cause;
253 if (detail)
254 out->failure_detail = *detail;
255 }
256 out->failed = true;
257 while (out->head) {
258 outbound_packet_t *p = out->head;
259 out->head = p->next;
260 out->bytes -= p->cost;
261 --out->count;
262 if (p->wait) {
263 p->result = out->failure_code;
264 p->done = true;
265 } else {
266 packet_free(out, p);
267 }
268 }
269 cond_broadcast(&out->changed);
270}
271
272static asciichat_error_t outbound_send(acip_outbound_t *out, const outbound_packet_t *packet) {
273 ASCII_OTEL_SCOPE_PARENT(ACIP_OUTBOUND_SEND, &packet->telemetry_context);
274 ASCII_OTEL_PACKET(packet->type, packet->size >= sizeof(packet_header_t) ? packet->size - sizeof(packet_header_t) : 0,
275 "send", acip_transport_get_type(out->transport));
276 uint64_t wait_ns = time_get_ns() - packet->created;
277 ASCII_OTEL_ATTR_INT("network.queue.wait_ns", wait_ns);
278 ASCII_OTEL_ATTR_STRING("network.packet.category", packet->video ? "video" : packet->audio ? "audio" : "control");
279 ascii_otel_metric(ASCII_OTEL_METRIC_OUTBOUND_WAIT, (double)wait_ns / 1e9);
280 if (out->transport->methods->sendv_ex) {
281 socket_buffer_t buffer = {packet->data, packet->size};
282 return out->transport->methods->sendv_ex(out->transport, &buffer, 1, &packet->options);
283 }
284 return out->transport->methods->send(out->transport, packet->data, packet->size);
285}
286
287static void *outbound_worker(void *arg) {
288 acip_outbound_t *out = arg;
289 mutex_lock(&out->mutex);
290 while (!out->stopping && !out->failed) {
291 uint64_t now = time_get_ns();
292 expire_video(out, now);
293 expire_datagrams(out, now);
294 outbound_packet_t *p = out->head;
295 if (!p) {
296 cond_timedwait(&out->changed, &out->mutex, 100 * NS_PER_MS_INT);
297 continue;
298 }
299 // Rotation uses this same serialized writer for every transport. Flush
300 // only when work is queued; idle polling must not reacquire the queue lock.
301 mutex_unlock(&out->mutex);
302 asciichat_error_t security_result = acip_security_flush(out->transport);
303 mutex_lock(&out->mutex);
304 if (security_result != ASCIICHAT_OK) {
305 outbound_record_failure_and_fail_pending(out, security_result, NULL, NULL);
306 break;
307 }
308 now = time_get_ns();
309 p = out->head;
310 if (!p)
311 continue;
312 /* Admitted reliable traffic expires by closing the connection, never by
313 * silently removing an audio or control packet. */
314 uint64_t deadline = p->audio ? 5 * NS_PER_SEC_INT : 10 * NS_PER_SEC_INT;
315 /* Reliable expiry is terminal for control traffic only. Video and audio
316 * are media queues and are governed by their media admission/expiry
317 * policy; they must not tear down the peer merely because a slow backend
318 * delayed submission. */
319 if (!p->video && !p->audio && p->options.delivery == ACIP_DELIVERY_RELIABLE && now - p->created > deadline) {
322 SET_ERRNO(ERROR_NETWORK_TIMEOUT, "Outbound reliable packet expired before submission");
323 bool has_cause = errno_peek_since(scope, &cause);
324 outbound_record_failure_and_fail_pending(out, ERROR_NETWORK_TIMEOUT, has_cause ? &cause : NULL, NULL);
326 break;
327 }
328 // Include backend-owned bytes before reserving room for audio and control.
329 size_t backend = backend_bytes(out);
330 // Keep scheduling decisions in the plaintext queue. Even tiny reliable
331 // packets must not accumulate behind video in a backend's opaque FIFO.
332 acip_transport_capabilities_t capabilities = {0};
333 asciichat_error_t capability_result = acip_transport_get_capabilities(out->transport, &capabilities);
334 if (capability_result != ASCIICHAT_OK) {
335 outbound_record_failure_and_fail_pending(out, capability_result, NULL, NULL);
336 break;
337 }
338 /* Media must wait for opaque backends without independent streams so it
339 * cannot build an unbounded FIFO behind an already-buffered frame. A
340 * reliable control packet is different: withholding it here can make it
341 * expire while waiting behind the media backlog it is meant to control.
342 * The backend preserves ordering, so submit control traffic and let it
343 * drain normally. */
344 if (backend != 0 && !capabilities.independent_streams && (p->video || p->audio)) {
345 if (out->transport->methods->wait_writable) {
346 mutex_unlock(&out->mutex);
347 asciichat_error_t writable = out->transport->methods->wait_writable(out->transport, 10 * NS_PER_MS_INT);
348 mutex_lock(&out->mutex);
349 if (writable != ASCIICHAT_OK && writable != ERROR_NETWORK_TIMEOUT) {
350 outbound_record_failure_and_fail_pending(out, writable, NULL, NULL);
351 break;
352 }
353 } else {
354 cond_timedwait(&out->changed, &out->mutex, 2 * NS_PER_MS_INT);
355 }
356 continue;
357 }
358 out->head = p->next;
359 out->bytes -= p->cost;
360 // Admission reserves plaintext plus two wire owners. During handoff one
361 // wire owner moves into the backend's counter, so do not count it twice.
362 out->active_bytes = p->cost - (p->size + WIRE_OVERHEAD);
363 mutex_unlock(&out->mutex);
364 /* One worker fixes wire/encryption order. Backends receive only complete
365 * packets and must finish a partially submitted packet or fail the stream. */
367 asciichat_error_t result = outbound_send(out, p);
368 asciichat_error_context_t cause = {0};
369 asciichat_error_context_t detail = {0};
370 if (result != ASCIICHAT_OK) {
371 // Only this send's matching failure belongs to the submission. A backend
372 // may return a status without context, or leave an unrelated nested error.
373 if (!errno_peek_since(scope, &cause) || cause.code != result) {
374 SET_ERRNO(result, "Outbound backend submission failed");
375 errno_peek_since(scope, &cause);
376 } else {
377 detail = backend_failure_detail(scope, &cause);
378 }
379 }
381 bool datagram_drop =
383 (result == ERROR_NETWORK_TIMEOUT || result == ERROR_BUFFER_FULL || result == ERROR_NOT_SUPPORTED);
384 if (!p->wait) {
385 mutex_lock(&out->mutex);
386 out->active_bytes = 0;
387 --out->count;
388 if (datagram_drop) {
389 if (p->encoded)
390 discard_stream(out, p);
391 atomic_fetch_add_u64(&out->dropped, 1);
392 ascii_otel_metric(ASCII_OTEL_METRIC_OUTBOUND_DROP, 1);
393 }
394 if (result == ASCIICHAT_OK) {
395 atomic_fetch_add_u64(&out->submitted, 1);
396 if (p->encoded && p->keyframe)
397 atomic_fetch_add_u64(&out->recovery_frames, 1);
398 }
399 if (result != ASCIICHAT_OK && !datagram_drop)
400 outbound_record_failure_and_fail_pending(out, result, &cause, &detail);
401 cond_broadcast(&out->changed);
402 packet_free(out, p);
403 mutex_unlock(&out->mutex);
404 if (result != ASCIICHAT_OK && !datagram_drop)
405 break;
406 continue;
407 }
408 mutex_lock(&out->mutex);
409 out->active_bytes = 0;
410 --out->count;
411 if (datagram_drop) {
412 if (p->encoded)
413 discard_stream(out, p);
414 atomic_fetch_add_u64(&out->dropped, 1);
415 ascii_otel_metric(ASCII_OTEL_METRIC_OUTBOUND_DROP, 1);
416 }
417 if (result == ASCIICHAT_OK) {
418 atomic_fetch_add_u64(&out->submitted, 1);
419 if (p->encoded && p->keyframe)
420 atomic_fetch_add_u64(&out->recovery_frames, 1);
421 }
422 if (p->wait) {
423 p->result = result;
424 p->done = true;
425 cond_broadcast(&out->changed);
426 }
427 if (result != ASCIICHAT_OK && !datagram_drop) {
428 outbound_record_failure_and_fail_pending(out, result, &cause, &detail);
429 break;
430 }
431 }
432 bool failed = out->failed && !out->stopped_by_caller;
433 mutex_unlock(&out->mutex);
434 if (failed)
435 out->transport->methods->close(out->transport);
436 return NULL;
437}
438
440 ASCII_OTEL_SCOPE(ACIP_OUTBOUND_CREATE);
441 if (!transport || !transport->methods || (!transport->methods->send && !transport->methods->sendv_ex))
442 return SET_ERRNO(ERROR_INVALID_PARAM, "Missing outbound backend");
443 if (transport->outbound)
444 return ASCIICHAT_OK;
446 if (mutex_init(&out->mutex, "connection_outgoing_packet_queue") != ASCIICHAT_OK) {
447 SAFE_FREE(out);
448 return GET_ERRNO();
449 }
450 if (cond_init(&out->changed, "outgoing_packet_ready_or_done") != ASCIICHAT_OK) {
451 mutex_destroy(&out->mutex);
452 SAFE_FREE(out);
453 return GET_ERRNO();
454 }
455 out->transport = transport;
456 asciichat_error_t result = platform_thread_create(&out->worker, "outbound", outbound_worker, out);
457 if (result != ASCIICHAT_OK) {
458 cond_destroy(&out->changed);
459 mutex_destroy(&out->mutex);
460 SAFE_FREE(out);
461 return result;
462 }
463 transport->outbound = out;
464 return ASCIICHAT_OK;
465}
466
468 acip_outbound_t *out = transport ? transport->outbound : NULL;
469 if (!out)
470 return;
471 mutex_lock(&out->mutex);
472 out->stopping = true;
473 out->stopped_by_caller = true;
474 outbound_record_failure_and_fail_pending(out, ERROR_NETWORK, NULL, NULL);
475 mutex_unlock(&out->mutex);
476}
477
479 ASCII_OTEL_SCOPE(ACIP_OUTBOUND_DESTROY);
480 acip_outbound_t *out = transport->outbound;
481 if (!out)
482 return;
483 mutex_lock(&out->mutex);
484 out->stopping = true;
485 cond_broadcast(&out->changed);
486 mutex_unlock(&out->mutex);
487 /* Wake a blocked send while every backend resource is still alive. The
488 * failed-send worker also closes its backend, so wait for that worker before
489 * closing here: two concurrent closes can join the same service thread. */
490 if (transport->methods->interrupt)
491 transport->methods->interrupt(transport);
492 else
493 transport->methods->close(transport);
494 thread_join(&out->worker, NULL);
495 if (transport->methods->interrupt)
496 transport->methods->close(transport);
497 mutex_lock(&out->mutex);
498 outbound_record_failure_and_fail_pending(out, ERROR_NETWORK, NULL, NULL);
499 while (out->recycled) {
500 outbound_packet_t *p = out->recycled;
501 out->recycled = p->next;
502 packet_free(out, p);
503 }
504 mutex_unlock(&out->mutex);
505 cond_destroy(&out->changed);
506 mutex_destroy(&out->mutex);
507 SAFE_FREE(transport->outbound);
508}
509
510static asciichat_error_t outbound_submitv(acip_transport_t *transport, const socket_buffer_t *buffers, size_t count,
511 const acip_send_options_t *options, acip_admission_t *admission) {
512 ASCII_OTEL_SCOPE(ACIP_OUTBOUND_SUBMITV);
513 if (!transport || !transport->outbound || !buffers || !count || count > SOCKET_IOV_MAX || !admission)
514 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid outbound submission");
515 *admission = ACIP_ADMISSION_CONGESTED;
516 size_t size = 0;
517 for (size_t i = 0; i < count; ++i) {
518 if ((buffers[i].len && !buffers[i].data) || buffers[i].len > ACIP_OUTBOUND_BUDGET - WIRE_OVERHEAD - size)
519 return SET_ERRNO(ERROR_NETWORK_SIZE, "Outbound packet exceeds byte budget");
520 size += buffers[i].len;
521 }
522 packet_header_t header;
523 if (buffers[0].len < sizeof(header))
524 return SET_ERRNO(ERROR_INVALID_PARAM, "Missing outbound ACIP header");
525 memcpy(&header, buffers[0].data, sizeof(header));
526 outbound_packet_t info = {.size = size,
527 .cost = 3 * (size + WIRE_OVERHEAD) + sizeof(outbound_packet_t) + 128,
528 .type = NET_TO_HOST_U16(header.type),
529 .stream = NET_TO_HOST_U32(header.client_id)};
530 info.options = options ? *options
531 : (acip_send_options_t){.traffic_class = acip_packet_traffic_class(info.type),
532 .delivery = ACIP_DELIVERY_RELIABLE};
533 if ((unsigned)info.options.traffic_class > ACIP_TRAFFIC_VISUAL ||
535 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid outbound send options");
537 if (!transport->methods->sendv_ex || !transport->methods->validate_send_options)
538 return SET_ERRNO(ERROR_NOT_SUPPORTED, "Datagrams and deadlines are not supported by this transport");
542 return SET_ERRNO(ERROR_INVALID_PARAM, "Datagram flow does not match packet type");
543 asciichat_error_t result = transport->methods->validate_send_options(transport, &info.options);
544 if (result != ASCIICHAT_OK)
545 return result;
546 if (info.options.deadline_ns && info.options.deadline_ns <= time_get_ns())
547 return SET_ERRNO(ERROR_NETWORK_TIMEOUT, "Outbound datagram deadline expired");
548 }
550 info.video = info.encoded || info.type == PACKET_TYPE_IMAGE_FRAME || info.type == PACKET_TYPE_ASCII_FRAME;
552 ASCII_OTEL_ATTR_STRING("network.packet.category", info.video ? "video" : info.audio ? "audio" : "control");
553 info.wait = !info.video && !info.audio && info.type != PACKET_TYPE_PING && info.type != PACKET_TYPE_PONG;
554 if (info.encoded) {
555 const uint8_t *flags = NULL;
556 if (buffers[0].len > sizeof(header))
557 flags = (const uint8_t *)buffers[0].data + sizeof(header);
558 else if (count > 1 && buffers[1].len)
559 flags = buffers[1].data;
560 if (!flags)
561 return SET_ERRNO(ERROR_INVALID_FRAME, "Missing encoded frame flags");
562 info.keyframe = (*flags & 1) != 0;
563 }
564 acip_outbound_t *out = transport->outbound;
565 mutex_lock(&out->mutex);
566 info.created = time_get_ns();
568 if (out->stopping || out->failed) {
569 asciichat_error_t code = out->failed ? out->failure_code : ERROR_NETWORK;
570 asciichat_error_context_t cause = out->failure;
571 asciichat_error_context_t detail = out->failure_detail;
572 mutex_unlock(&out->mutex);
573 return cause.code == code ? import_failure(&cause, &detail) : SET_ERRNO(code, "Outbound connection closed");
574 }
575 // Admission first retires stale plaintext and restores codec recovery state;
576 // dependent predictive frames stay rejected until a fresh keyframe arrives.
577 expire_video(out, info.created);
578 expire_datagrams(out, info.created);
579 outbound_stream_t *stream = info.encoded ? stream_state(out, info.type, info.stream) : NULL;
580 if (info.encoded && (!stream || (atomic_load_bool(&stream->needs_keyframe) && !info.keyframe))) {
581 atomic_fetch_add_u64(&out->dropped, 1);
582 ascii_otel_metric(ASCII_OTEL_METRIC_OUTBOUND_DROP, 1);
584 ASCII_OTEL_ATTR_STRING("network.queue.admission", "needs_keyframe");
585 mutex_unlock(&out->mutex);
586 return ASCIICHAT_OK;
587 }
588 if (info.video && !info.encoded)
589 discard_stream(out, &info);
590 // Include backend-owned bytes before reserving room for audio and control.
591 size_t backend = backend_bytes(out);
594 : 0;
595 size_t limit = ACIP_OUTBOUND_BUDGET - reserved;
596 size_t pending = out->bytes + out->active_bytes + backend + transport->methods->reserved_bytes;
597 ASCII_OTEL_ATTR_INT("network.queue.pending_bytes", (int64_t)pending);
598 ASCII_OTEL_ATTR_INT("network.queue.budget_bytes", (int64_t)limit);
599 ascii_otel_metric(ASCII_OTEL_METRIC_OUTBOUND_PENDING, (double)pending);
600 if (out->head)
601 ascii_otel_metric(ASCII_OTEL_METRIC_OUTBOUND_AGE, (double)(info.created - out->head->created) / 1e9);
602 if (out->count >= OUTBOUND_MAX_PACKETS - (info.video ? 64
603 : info.audio ? 32
604 : 0) ||
605 pending > limit || info.cost > limit - pending) {
606 if (info.encoded)
607 discard_stream(out, &info);
608 atomic_fetch_add_u64(&out->congestion, 1);
609 ASCII_OTEL_ATTR_STRING("network.queue.admission", "congested");
610 ascii_otel_metric(ASCII_OTEL_METRIC_OUTBOUND_CONGESTION, 1);
611 mutex_unlock(&out->mutex);
612 return ASCIICHAT_OK;
613 }
614 // Reuse storage only when ownership and synchronous-wait semantics match.
615 outbound_packet_t **cached = &out->recycled;
616 while (*cached && ((*cached)->wait != info.wait || buffer_capacity((*cached)->data) < size))
617 cached = &(*cached)->next;
618 outbound_packet_t *p = *cached;
619 uint8_t *storage = NULL;
620 if (p) {
621 *cached = p->next;
622 --out->recycled_count;
623 storage = p->data;
624 } else {
625 p = SAFE_MALLOC(sizeof(*p), outbound_packet_t *);
626 }
627 *p = info;
628 p->data = storage ? storage : buffer_alloc(info.wait ? buffer_control_domain() : NULL, size);
629 if (!p->data) {
630 SAFE_FREE(p);
631 mutex_unlock(&out->mutex);
632 return SET_ERRNO(ERROR_MEMORY, "Cannot own outbound plaintext");
633 }
634 size_t offset = 0;
635 for (size_t i = 0; i < count; ++i) {
636 if (buffers[i].len)
637 memcpy(p->data + offset, buffers[i].data, buffers[i].len);
638 offset += buffers[i].len;
639 }
640 /* All non-video traffic is inserted ahead of pending media while preserving
641 * protocol barriers and FIFO order within each traffic class. */
642 bool was_empty = outbound_insert_packet(out, p, !info.video);
643 out->bytes += info.cost;
644 ++out->count;
645 atomic_fetch_add_u64(&out->accepted, 1);
646 pending += info.cost;
647 uint64_t peak = atomic_load_u64(&out->peak_bytes);
648 while (pending > peak && !atomic_cas_u64(&out->peak_bytes, &peak, pending))
649 ;
650 if (stream && info.keyframe)
651 atomic_store_bool(&stream->needs_keyframe, false);
652 *admission = ACIP_ADMISSION_ACCEPTED;
653 ASCII_OTEL_ATTR_STRING("network.queue.admission", "accepted");
654 if (was_empty)
655 cond_broadcast(&out->changed);
657 asciichat_error_context_t cause = {0};
658 asciichat_error_context_t detail = {0};
659 if (info.wait) {
660 while (!p->done)
661 cond_wait(&out->changed, &out->mutex);
662 result = p->result;
663 if (result != ASCIICHAT_OK) {
664 cause = out->failure;
665 detail = out->failure_detail;
666 }
667 packet_free(out, p);
668 }
669 mutex_unlock(&out->mutex);
670 if (result == ASCIICHAT_OK)
671 return result;
672 return cause.code == result ? import_failure(&cause, &detail) : SET_ERRNO(result, "Outbound submission failed");
673}
674
675asciichat_error_t acip_outbound_submitv(acip_transport_t *transport, const socket_buffer_t *buffers, size_t count,
676 acip_admission_t *admission) {
677 return outbound_submitv(transport, buffers, count, NULL, admission);
678}
679
680asciichat_error_t acip_outbound_submitv_ex(acip_transport_t *transport, const socket_buffer_t *buffers, size_t count,
681 const acip_send_options_t *options, acip_admission_t *admission) {
682 if (!options)
683 return SET_ERRNO(ERROR_INVALID_PARAM, "Missing outbound send options");
684 return outbound_submitv(transport, buffers, count, options, admission);
685}
686
688 if (!transport || !transport->outbound || !stats)
689 return SET_ERRNO(ERROR_INVALID_PARAM, "Missing outbound statistics");
690 acip_outbound_t *out = transport->outbound;
691 mutex_lock(&out->mutex);
692 stats->accepted = atomic_load_u64(&out->accepted);
693 stats->submitted = atomic_load_u64(&out->submitted);
694 stats->dropped = atomic_load_u64(&out->dropped);
695 stats->congestion = atomic_load_u64(&out->congestion);
696 stats->recovery_frames = atomic_load_u64(&out->recovery_frames);
697 stats->peak_bytes = (size_t)atomic_load_u64(&out->peak_bytes);
698 stats->application_bytes = out->bytes + out->active_bytes;
699 stats->backend_bytes = backend_bytes(out);
700 stats->backend_reserved_bytes = transport->methods->reserved_bytes;
701 uint64_t now = time_get_ns();
702 for (outbound_packet_t *p = out->head; p; p = p->next)
703 if (p->video && now - p->created > stats->oldest_frame_age_ns)
704 stats->oldest_frame_age_ns = now - p->created;
705 mutex_unlock(&out->mutex);
706 return ASCIICHAT_OK;
707}
708
710 if (!transport || !transport->outbound)
711 return false;
712 acip_outbound_t *out = transport->outbound;
713 if (transport->methods->consume_keyframe_request &&
714 transport->methods->consume_keyframe_request(transport, type, id)) {
715 outbound_packet_t stream = {.type = type, .stream = id, .video = true, .encoded = true};
716 mutex_lock(&out->mutex);
717 discard_stream(out, &stream);
718 mutex_unlock(&out->mutex);
719 return true;
720 }
721 mutex_lock(&out->mutex);
722 size_t count = (size_t)atomic_load_u64(&out->stream_count);
723 for (size_t i = 0; i < count; ++i)
724 if (out->streams[i].type == type && out->streams[i].stream == id) {
725 bool needs_keyframe = atomic_load_bool(&out->streams[i].needs_keyframe);
726 mutex_unlock(&out->mutex);
727 return needs_keyframe;
728 }
729 mutex_unlock(&out->mutex);
730 return true;
731}
asciichat_error_t errno_set_with_message(asciichat_error_t code, const char *file, int line, const char *function, const char *format,...)
bool errno_peek_since(asciichat_errno_scope_t scope, asciichat_error_context_t *out)
void errno_scope_end(asciichat_errno_scope_t scope, asciichat_errno_outcome_t outcome)
asciichat_errno_snapshot_t errno_snapshot(uint64_t id, asciichat_error_context_t *out, size_t capacity)
asciichat_errno_scope_t errno_scope_begin(void)
asciichat_error_t errno_import(const asciichat_error_context_t *cause)
#define SET_ERRNO(code,...)
#define GET_ERRNO()
@ ASCIICHAT_ERRNO_HANDLED
#define ASCIICHAT_ERRNO_MAX_DEPTH
void acip_outbound_destroy(acip_transport_t *transport)
asciichat_error_t acip_outbound_submitv_ex(acip_transport_t *transport, const socket_buffer_t *buffers, size_t count, const acip_send_options_t *options, acip_admission_t *admission)
asciichat_error_t acip_outbound_snapshot(acip_transport_t *transport, acip_outbound_stats_t *stats)
asciichat_error_t acip_outbound_create(acip_transport_t *transport)
struct outbound_packet outbound_packet_t
asciichat_error_t acip_outbound_submitv(acip_transport_t *transport, const socket_buffer_t *buffers, size_t count, acip_admission_t *admission)
#define OUTBOUND_MAX_STREAMS
#define OUTBOUND_MAX_PACKETS
bool acip_outbound_needs_keyframe(acip_transport_t *transport, uint16_t type, uint32_t id)
#define WIRE_OVERHEAD
void acip_outbound_stop(acip_transport_t *transport)
#define ACIP_OUTBOUND_VIDEO_AGE_NS
#define ACIP_OUTBOUND_BUDGET
struct acip_outbound acip_outbound_t
#define ACIP_OUTBOUND_AUDIO_RESERVE
acip_admission_t
@ ACIP_ADMISSION_ACCEPTED
@ ACIP_ADMISSION_CONGESTED
@ ACIP_ADMISSION_NEEDS_KEYFRAME
#define ACIP_OUTBOUND_CONTROL_RESERVE
size_t buffer_capacity(const void *data)
Definition buffer.c:280
void * buffer_alloc(buffer_domain_t *domain, size_t size)
Definition buffer.c:274
buffer_domain_t * buffer_control_domain(void)
Definition buffer.c:436
#define buffer_release(data)
Definition buffer.h:91
⚙️ Common definitions, error codes, macros, and types shared throughout the application
🔄 Network byte order conversion helpers
#define NET_TO_HOST_U16(val)
Definition endian.h:111
#define NET_TO_HOST_U32(val)
Definition endian.h:81
unsigned short uint16_t
Definition common.h:61
unsigned int uint32_t
Definition common.h:62
#define SAFE_FREE(ptr)
Definition common.h:377
#define SAFE_MALLOC(size, cast)
Definition common.h:265
#define SAFE_CALLOC(count, size, cast)
Definition common.h:275
unsigned long long uint64_t
Definition common.h:63
unsigned char uint8_t
Definition common.h:60
asciichat_error_t
Error and exit codes - unified status values (0-255).
Definition error_codes.h:44
@ ERROR_NETWORK
Definition error_codes.h:72
@ ERROR_BUFFER_FULL
@ ERROR_MEMORY
Definition error_codes.h:51
@ ASCIICHAT_OK
Definition error_codes.h:46
@ ERROR_NETWORK_SIZE
Definition error_codes.h:77
@ ERROR_INVALID_FRAME
@ ERROR_INVALID_PARAM
@ ERROR_NETWORK_TIMEOUT
Definition error_codes.h:75
@ ERROR_NOT_SUPPORTED
Definition error_codes.h:69
#define log_warn(...)
Log a WARN message.
Definition log/log.h:621
#define NS_PER_SEC_INT
Definition time.h:169
#define NS_PER_MS_INT
Definition time.h:168
@ PACKET_TYPE_IMAGE_FRAME
Complete RGB image with dimensions.
Definition packet.h:385
@ PACKET_TYPE_PONG
Keepalive pong response.
Definition packet.h:409
@ PACKET_TYPE_IMAGE_FRAME_H265
H.265-encoded frame from client (HEVC/H.265 codec).
Definition packet.h:387
@ PACKET_TYPE_ASCII_FRAME
Complete ASCII frame with all metadata.
Definition packet.h:383
@ PACKET_TYPE_IMAGE_FRAME_H264
Definition packet.h:389
@ PACKET_TYPE_PING
Keepalive ping packet.
Definition packet.h:407
struct acip_transport acip_transport_t
Forward declaration of ACIP transport.
Definition host.h:506
Transport abstraction layer for ACIP protocol.
asciichat_error_t acip_security_flush(acip_transport_t *transport)
gotel_context_t ascii_otel_context(void)
Definition otel.c:725
void ascii_otel_metric(ascii_otel_metric_t metric, double value)
Definition otel.c:161
#define ASCII_OTEL_SCOPE(id)
Definition otel.h:72
#define ASCII_OTEL_SCOPE_PARENT(id, parent)
Definition otel.h:69
#define ASCII_OTEL_PACKET(type, size, action, transport)
Definition otel.h:68
#define ASCII_OTEL_ATTR_INT(key, value)
Definition otel.h:67
#define ASCII_OTEL_ATTR_STRING(key, value)
Definition otel.h:66
static bool size_t index
Definition queries.h:8
static bool size_t gotel_query_t * out
Definition queries.h:8
asciichat_error_t acip_transport_get_capabilities(acip_transport_t *transport, acip_transport_capabilities_t *out)
Definition send.c:104
🔤 String Manipulation and Shell Escaping Utilities
This is the handoff point for acip outbound stats. Callers fill application_bytes,...
atomic_t recovery_frames
atomic_t submitted
size_t recycled_count
atomic_t stream_count
outbound_packet_t * recycled
platform_thread_t worker
outbound_packet_t * head
size_t active_bytes
bool stopped_by_caller
acip_transport_t * transport
atomic_t dropped
atomic_t accepted
outbound_stream_t streams[64]
asciichat_error_context_t failure
atomic_t peak_bytes
asciichat_error_t failure_code
asciichat_error_context_t failure_detail
atomic_t congestion
uint64_t deadline_ns
Definition traffic.h:19
acip_delivery_t delivery
Definition traffic.h:18
acip_traffic_class_t traffic_class
Definition traffic.h:17
The useful part of acip transport capabilities is in independent_streams, datagrams,...
asciichat_error_t(* validate_send_options)(acip_transport_t *transport, const acip_send_options_t *options)
void(* interrupt)(acip_transport_t *transport)
asciichat_error_t(* send)(acip_transport_t *transport, const void *data, size_t len)
Send data through this transport.
asciichat_error_t(* close)(acip_transport_t *transport)
Close this transport.
bool(* consume_keyframe_request)(acip_transport_t *transport, uint16_t type, uint32_t stream_id)
size_t reserved_bytes
Bound for backend-owned pending data not exposed by queued_bytes.
asciichat_error_t(* sendv_ex)(acip_transport_t *transport, const socket_buffer_t *buffers, size_t count, const acip_send_options_t *options)
struct acip_outbound * outbound
Per-connection plaintext admission and scheduling.
const acip_transport_methods_t * methods
Method table (virtual functions).
When code needs to work with asciichat errno snapshot, it keeps total, copied here instead of scatter...
gotel_context_t telemetry_context
struct outbound_packet * next
asciichat_error_t result
acip_send_options_t options
uint64_t last_used_ns
atomic_t needs_keyframe
Fixed prefix shared by every ACIP packet on the wire.
Definition packet.h:628
⏱️ High-precision timing utilities using sokol_time.h and uthash
acip_traffic_class_t acip_packet_traffic_class(uint16_t type)
Definition traffic.c:4
@ ACIP_TRAFFIC_CONTROL
Definition traffic.h:6
@ ACIP_TRAFFIC_VISUAL
Definition traffic.h:8
@ ACIP_TRAFFIC_AUDIO
Definition traffic.h:7
@ ACIP_DELIVERY_RELIABLE
Definition traffic.h:12
@ ACIP_DELIVERY_DATAGRAM
Definition traffic.h:13