fix: a full reorder ring keeps the arriving packet and counts the overflow, instead of dropping it silently
A packet that needed a slot and found none was neither held nor delivered: no counter recorded it and the consumer reserved a hole nothing could ever backfill. It now commits the gap to free a slot and takes the ordering decision once more against the sequence that commit left behind, bounded to one retry; past that it is delivered in order with the counters it skipped as its `lost`, never flagged late, because a packet ahead of the sequence has no hole to be looked up by. A duplicate can now replace its slot at a full ring, as hold_packet's own comment already promised, and the 20 ms default window is a named constant. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
cb1bbd2b7b
commit
176ef1df9f
6 files changed
+113
-38
No files matched your search
Vendored
+79
-33
@@ -37,6 +37,8 @@
|
||||
#define UDPS_REORDER_SLOTS 64
|
||||
/** Total bytes the reorder ring may hold, so a fast stream cannot grow it without bound. */
|
||||
#define UDPS_REORDER_BYTES (4u * 1024u * 1024u)
|
||||
/** How long a gap is given to resolve before it is committed, by default. */
|
||||
#define UDPS_REORDER_WINDOW_S_DEFAULT 0.020
|
||||
|
||||
/** Concurrent in-flight reassemblies. The server interleaves at most a couple. */
|
||||
#define UDPS_MAX_SLOTS 4
|
||||
@@ -576,20 +578,16 @@ static int deliver_data(udps_client_t *c, const uint8_t *pl, size_t len,
|
||||
*
|
||||
* A duplicate counter (same datagram arriving twice on the wire) replaces the
|
||||
* existing slot so one counter never consumes two slots and both get delivered.
|
||||
* The search for that slot therefore runs *before* the capacity checks, and the
|
||||
* checks are applied only on the path that needs a new slot: a duplicate at a
|
||||
* full ring consumes nothing and must still be accepted.
|
||||
* Returns non-zero and holds nothing when there is no room (ring full or byte
|
||||
* budget exhausted). */
|
||||
* budget exhausted); `order_data` below says what the caller then does. */
|
||||
static int hold_packet(udps_client_t *c, const uint8_t *pl, size_t len,
|
||||
uint32_t counter, double recv_time) {
|
||||
int i;
|
||||
int slot = -1;
|
||||
|
||||
if (c->held_count >= UDPS_REORDER_SLOTS) {
|
||||
return -1;
|
||||
}
|
||||
if (c->held_bytes + len > UDPS_REORDER_BYTES) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Duplicate counter: reuse the existing slot to avoid double delivery. */
|
||||
for (i = 0; i < UDPS_REORDER_SLOTS; i++) {
|
||||
if (c->held[i].used && c->held[i].counter == counter) {
|
||||
@@ -597,16 +595,28 @@ static int hold_packet(udps_client_t *c, const uint8_t *pl, size_t len,
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (slot < 0) {
|
||||
if (slot >= 0) {
|
||||
/* The replacement releases the length already held, so the budget is
|
||||
* weighed against what the slot will cost rather than what it adds. */
|
||||
if (c->held_bytes - c->held[slot].len + len > UDPS_REORDER_BYTES) {
|
||||
return -1;
|
||||
}
|
||||
} else {
|
||||
if (c->held_count >= UDPS_REORDER_SLOTS) {
|
||||
return -1;
|
||||
}
|
||||
if (c->held_bytes + len > UDPS_REORDER_BYTES) {
|
||||
return -1;
|
||||
}
|
||||
for (i = 0; i < UDPS_REORDER_SLOTS; i++) {
|
||||
if (!c->held[i].used) {
|
||||
slot = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (slot < 0) {
|
||||
return -1;
|
||||
if (slot < 0) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
if (ensure_cap((void **)&c->held[slot].buf, &c->held[slot].cap, len) != 0) {
|
||||
@@ -717,10 +727,27 @@ static int commit_gap(udps_client_t *c) {
|
||||
* milliseconds, and holding is what turns it back into order. A packet behind the
|
||||
* sequence lost that race: the consumer already has a hole where it belongs, so it
|
||||
* is delivered flagged rather than discarded, which is the whole point -- a dropped
|
||||
* packet cannot fill the hole it left. */
|
||||
* packet cannot fill the hole it left.
|
||||
*
|
||||
* The full ring is specified here, in one place. A packet that needs a slot and
|
||||
* finds none gives up on the current gap -- `commit_gap()` delivers the lowest held
|
||||
* run and so frees at least one slot -- counts the event in
|
||||
* `stats.reorder_overflows`, and the decision is then taken again, once, against
|
||||
* the sequence that commit left behind. That re-decision is why this is a bounded
|
||||
* loop and not a straight line: the commit advances `next_counter` to the lowest
|
||||
* held counter, which may put it past the arriving packet, and the packet is then
|
||||
* behind the sequence rather than ahead of it. Whatever happens, the arriving
|
||||
* packet is held or delivered; it is never dropped, because nothing else records it
|
||||
* and the consumer would reserve a hole for it that nothing could ever backfill.
|
||||
* Past the cap it is delivered *in order* and not flagged late: a packet ahead of
|
||||
* the sequence has no reserved hole, the consumer looks a late packet's hole up by
|
||||
* counter and ignores one it cannot place, so advancing `next_counter` to it and
|
||||
* reporting the counters it skipped as `lost` is what reserves their hole. */
|
||||
static int order_data(udps_client_t *c, const uint8_t *pl, size_t len,
|
||||
uint32_t counter, double recv_time) {
|
||||
int32_t delta;
|
||||
int attempt;
|
||||
int rc;
|
||||
|
||||
if (c->cfg.reorder_window_s <= 0.0) {
|
||||
/* Resequencing off: byte-for-byte the behaviour before this task. `delta`
|
||||
@@ -744,27 +771,46 @@ static int order_data(udps_client_t *c, const uint8_t *pl, size_t len,
|
||||
c->next_counter = counter + 1u;
|
||||
return deliver_data(c, pl, len, counter, recv_time, 0u, 0);
|
||||
}
|
||||
for (attempt = 0; attempt < 2; attempt++) {
|
||||
delta = (int32_t)(counter - c->next_counter);
|
||||
if (delta == 0) {
|
||||
rc = deliver_data(c, pl, len, counter, recv_time, 0u, 0);
|
||||
c->next_counter = counter + 1u;
|
||||
if (rc == 0) { rc = drain_held(c, 0u); }
|
||||
return rc;
|
||||
}
|
||||
if (delta < 0) {
|
||||
/* Behind the sequence. */
|
||||
c->stats.late_delivered++;
|
||||
return deliver_data(c, pl, len, counter, recv_time, 0u, 1);
|
||||
}
|
||||
if (hold_packet(c, pl, len, counter, recv_time) == 0) {
|
||||
if (c->gap_deadline == 0.0) {
|
||||
c->gap_deadline = recv_time + c->cfg.reorder_window_s;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
/* No room: give up on the gap rather than growing the ring, and say so
|
||||
* in a counter, so an undersized window is a reading and not a mystery. */
|
||||
c->stats.reorder_overflows++;
|
||||
rc = commit_gap(c);
|
||||
if (rc != 0) {
|
||||
/* The consumer refused a delivery; nothing is freed and nothing more
|
||||
* may be delivered, so the arriving packet is its caller's problem. */
|
||||
return rc;
|
||||
}
|
||||
}
|
||||
/* The attempt cap is spent and the ring still has no room (the byte budget
|
||||
* cannot fit this one even with a slot free). Deliver it rather than drop it. */
|
||||
delta = (int32_t)(counter - c->next_counter);
|
||||
if (delta == 0) {
|
||||
int rc = deliver_data(c, pl, len, counter, recv_time, 0u, 0);
|
||||
c->next_counter = counter + 1u;
|
||||
if (rc == 0) { rc = drain_held(c, 0u); }
|
||||
return rc;
|
||||
if (delta < 0) {
|
||||
c->stats.late_delivered++;
|
||||
return deliver_data(c, pl, len, counter, recv_time, 0u, 1);
|
||||
}
|
||||
if (delta > 0) {
|
||||
if (hold_packet(c, pl, len, counter, recv_time) != 0) {
|
||||
/* No room: commit the gap now rather than growing. The counter is
|
||||
* visible, so an undersized window is a reading and not a mystery. */
|
||||
return commit_gap(c);
|
||||
}
|
||||
if (c->gap_deadline == 0.0) {
|
||||
c->gap_deadline = recv_time + c->cfg.reorder_window_s;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
/* delta < 0: behind the sequence. */
|
||||
c->stats.late_delivered++;
|
||||
return deliver_data(c, pl, len, counter, recv_time, 0u, 1);
|
||||
rc = deliver_data(c, pl, len, counter, recv_time, (uint32_t)delta, 0);
|
||||
c->next_counter = counter + 1u;
|
||||
if (rc == 0) { rc = drain_held(c, 0u); }
|
||||
return rc;
|
||||
}
|
||||
|
||||
/*---------------------------------------------------------------------------*/
|
||||
@@ -1187,7 +1233,7 @@ void udps_client_config_init(udps_client_config_t *cfg) {
|
||||
cfg->keepalive_interval_s = 15.0; /* server evicts silent clients at 30 s */
|
||||
cfg->recv_buffer_bytes = 4u * 1024u * 1024u;
|
||||
cfg->max_packet_bytes = 1u * 1024u * 1024u;
|
||||
cfg->reorder_window_s = 0.020;
|
||||
cfg->reorder_window_s = UDPS_REORDER_WINDOW_S_DEFAULT;
|
||||
}
|
||||
|
||||
static void copy_str(char *dst, size_t cap, const char *src) {
|
||||
|
||||
Reference in new issue
Block a user