helper: Drop packets when the peer stops reading from the socket - #266
helper: Drop packets when the peer stops reading from the socket#266leolannenmaki wants to merge 2 commits into
Conversation
The test fills its own receive buffer by sending pings to the gateway without reading the replies, and expects the helper to report dropped packets and to keep forwarding once the client reads again. It fails on current code: when sendmsg_x() or write() fail with ENOBUFS, write_to_vm() retries forever, so a client that stops reading from the socket blocks the host->vm forwarding queue indefinitely, delaying every flow behind it. A helper blocked this way cannot even stop: on SIGTERM the atexit handler stops the vmnet interface on the same blocked queue, so the helper never exits. The test reads the backlog before stopping the helper to avoid hanging on failure.
write_to_vm() retried ENOBUFS forever, 50 microseconds at a time. The
vm socket is a datagram socket, so ENOBUFS means the peer receive
buffer is full: the client (vfkit, krunkit) is not draining the socket
as fast as the host sends. Since all host->vm forwarding runs on one
serial queue, one slow or stalled client blocked every host->vm packet
behind it with no bound, delaying unrelated flows and latency
sensitive frames such as TCP acks and HTTP/2 pings. Real clients hit
this: a guest that drains slowly during large concurrent downloads can
hold the queue full for seconds, and HTTP/2 clients in the guest time
out their connection health pings and drop the connection with all its
streams.
Bound the wait to VM_MAX_RETRIES (5 ms), shared by the whole batch.
When the budget is exhausted every remaining packet gets one last
write() before being dropped, since a smaller packet may still fit in
the peer receive buffer. Dropping is safe: ethernet does not guarantee
delivery, the guest transport protocols recover from the loss, and TCP
backs off, which keeps the queue short. Dropped packets are counted
and logged at most once per second.
Example:
WARN [host->vm] dropped 11 packets (11 total): peer is not reading from the socket
|
@leolannenmaki Thanks for the PR!
I agree the current behaviour is simplistic, but I never seen an issue in real use. libkrun uses a similar solution - packets are never dropped.
This is easy to fix, we can check if signal was received in the wait loop.
This is the most important thing - can you open an issue and explain how to reproduce this and which real world use cases are affected?
Makes sense to limit the time we wait. Do you know how real switches handle the same? how much time a packet is waiting in the switch buffers when the receiver is not fast enough? |
When the kernel's vmnet buffer is full, the helper retries
sendmsg_x() with a 50 microsecond sleep between attempts. Previously
the retries were unbounded, causing head-of-line blocking: a
high-priority small packet (e.g. an HTTP/2 PING or TCP ACK) queued
behind large data packets is delayed without bound while the helper
waits for buffer space. If the delay exceeds the protocol's timeout,
the connection breaks even though the network is otherwise healthy.
Limit retries to 10 attempts (500µs maximum wait) shared across the
entire batch. The fast path retries sendmsg_x() within this budget,
then falls back to the slow path which writes remaining packets one
at a time, also retrying within the same budget. Packets are dropped
only after the budget is exhausted or on non-ENOBUFS errors.
Before (ENOBUFS in fast path):
sendmsg_x(batch) → ENOBUFS
→ sleep 50µs → retry → ENOBUFS
→ sleep 50µs → retry → ...
(unbounded retries, blocks all packets in the batch)
After (ENOBUFS in fast path):
sendmsg_x(batch) → ENOBUFS
→ sleep 50µs → retry (up to 10 times, shared budget)
→ fall back to slow path
→ write(packet₁) → ENOBUFS → retry (same budget)
→ write(packet₁) → success
→ write(packet₂) → ENOBUFS → budget exhausted → drop
→ write(packet₃) → success
(bounded wait across both paths, then drop)
The retry limit is per batch, not per sendmsg_x call, ensuring the
total wait is bounded regardless of partial successes between
retries.
Dropping packets is correct because the helper implements an ethernet
link between the VM and the host. Like a physical network link under
congestion, packets may be lost. TCP retransmits lost segments, and
UDP applications are expected to tolerate loss.
Bounded retries are preferable to dropping immediately because TCP
retransmission is far more expensive than a 50µs sleep. Dropping
causes TCP to halve its congestion window and wait for retransmit,
taking milliseconds to seconds to recover. In testing, dropping
immediately on ENOBUFS caused throughput to collapse from 26 Gbps to
20 Gbps with 4 parallel streams as TCP repeatedly backed off.
ENOBUFS occurs when the VM cannot drain the vmnet buffer fast enough.
With a fast VMM (krunkit) and --busy-poll, the VM processes packets
fast enough that ENOBUFS never occurs even at 36 Gbps:
krunkit, --busy-poll, 4 CPUs, 8 MiB buffer:
TX (Gbps) drops fast calls/sec
before (retry) 36.4 0 14.5k
after (drop) 36.4 0 14.5k
With a slower VMM (vfkit), the VM cannot keep up and ENOBUFS occurs.
The bounded retry mechanism absorbs most ENOBUFS pressure, but some
packets are dropped. The previous unbounded retry behavior achieved
slightly higher throughput for vfkit by naturally throttling the
sending rate:
vfkit, --busy-poll, 4 CPUs, 4 MiB buffer:
TX (Gbps) drops fast calls/sec
before (retry) 9.8 0 15-28k
after (drop) 9.4 780 ~28k
The slight throughput regression for slow VMMs is acceptable because
head-of-line blocking is a correctness issue, not a performance
issue. Without bounded retries, a single blocked batch can stall all
traffic for an unbounded duration.
Based on #266 with the following changes:
- Tighter retry budget: 10 retries (500µs) vs 100 (5ms). Benchmarks
show retries rarely exceed a few attempts.
- Change error logging in sendmsg_x() and write() from ERROR to DEBUG
for ENOBUFS (normal backpressure signal) and WARN for other errors
(unexpected but recoverable).
- Drops tracked via stats counters instead of a separate rate-limited
warning log.
Thanks: Leo Lännenmäki <leo.lannenmaki@avrea.com>
Fixes: #267
When the kernel's vmnet buffer is full, the helper retries
sendmsg_x() with a 50 microsecond sleep between attempts. Previously
the retries were unbounded, causing head-of-line blocking: a
high-priority small packet (e.g. an HTTP/2 PING or TCP ACK) queued
behind large data packets is delayed without bound while the helper
waits for buffer space. If the delay exceeds the protocol's timeout,
the connection breaks even though the network is otherwise healthy.
Limit retries to 10 attempts (500µs maximum wait) shared across the
entire batch. The fast path retries sendmsg_x() within this budget,
then falls back to the slow path which writes remaining packets one
at a time, also retrying within the same budget. Packets are dropped
only after the budget is exhausted or on non-ENOBUFS errors.
Before (ENOBUFS in fast path):
sendmsg_x(batch) → ENOBUFS
→ sleep 50µs → retry → ENOBUFS
→ sleep 50µs → retry → ...
(unbounded retries, blocks all packets in the batch)
After (ENOBUFS in fast path):
sendmsg_x(batch) → ENOBUFS
→ sleep 50µs → retry (up to 10 times, shared budget)
→ fall back to slow path
→ write(packet₁) → ENOBUFS → retry (same budget)
→ write(packet₁) → success
→ write(packet₂) → ENOBUFS → budget exhausted → drop
→ write(packet₃) → success
(bounded wait across both paths, then drop)
The retry limit is per batch, not per sendmsg_x call, ensuring the
total wait is bounded regardless of partial successes between
retries.
Dropping packets is correct because the helper implements an ethernet
link between the VM and the host. Like a physical network link under
congestion, packets may be lost. TCP retransmits lost segments, and
UDP applications are expected to tolerate loss.
Bounded retries are preferable to dropping immediately because TCP
retransmission is far more expensive than a 50µs sleep. Dropping
causes TCP to halve its congestion window and wait for retransmit,
taking milliseconds to seconds to recover. In testing, dropping
immediately on ENOBUFS caused throughput to collapse from 26 Gbps to
20 Gbps with 4 parallel streams as TCP repeatedly backed off.
ENOBUFS occurs when the VM cannot drain the vmnet buffer fast enough.
With a fast VMM (krunkit) and --busy-poll, the VM processes packets
fast enough that ENOBUFS never occurs even at 36 Gbps:
krunkit, --busy-poll, 4 CPUs, 8 MiB buffer:
TX (Gbps) drops fast calls/sec
before (retry) 36.4 0 14.5k
after (drop) 36.4 0 14.5k
With a slower VMM (vfkit), the VM cannot keep up and ENOBUFS occurs.
The bounded retry mechanism absorbs most ENOBUFS pressure, but some
packets are dropped. The previous unbounded retry behavior achieved
slightly higher throughput for vfkit by naturally throttling the
sending rate:
vfkit, --busy-poll, 4 CPUs, 4 MiB buffer:
TX (Gbps) drops fast calls/sec
before (retry) 9.8 0 15-28k
after (drop) 9.4 780 ~28k
The slight throughput regression for slow VMMs is acceptable because
head-of-line blocking is a correctness issue, not a performance
issue. Without bounded retries, a single blocked batch can stall all
traffic for an unbounded duration.
Based on #266 with the following changes:
- Tighter retry budget: 10 retries (500µs) vs 100 (5ms). Benchmarks
show retries rarely exceed a few attempts.
- Change error logging in sendmsg_x() and write() from ERROR to DEBUG
for ENOBUFS (normal backpressure signal) and WARN for other errors
(unexpected but recoverable).
- Drops tracked via stats counters instead of a separate rate-limited
warning log.
Thanks: Leo Lännenmäki <leo.lannenmaki@avrea.com>
Fixes: #267
When the kernel's vmnet buffer is full, the helper retries
sendmsg_x() with a 50 microsecond sleep between attempts. Previously
the retries were unbounded, causing head-of-line blocking: a
high-priority small packet (e.g. an HTTP/2 PING or TCP ACK) queued
behind large data packets is delayed without bound while the helper
waits for buffer space. If the delay exceeds the protocol's timeout,
the connection breaks even though the network is otherwise healthy.
Limit retries to 10 attempts (500µs maximum wait) shared across the
entire batch. The fast path retries sendmsg_x() within this budget,
then falls back to the slow path which writes remaining packets one
at a time, also retrying within the same budget. Packets are dropped
only after the budget is exhausted or on non-ENOBUFS errors.
Before (ENOBUFS in fast path):
sendmsg_x(batch) → ENOBUFS
→ sleep 50µs → retry → ENOBUFS
→ sleep 50µs → retry → ...
(unbounded retries, blocks all packets in the batch)
After (ENOBUFS in fast path):
sendmsg_x(batch) → ENOBUFS
→ sleep 50µs → retry (up to 10 times, shared budget)
→ fall back to slow path
→ write(packet₁) → ENOBUFS → retry (same budget)
→ write(packet₁) → success
→ write(packet₂) → ENOBUFS → budget exhausted → drop
→ write(packet₃) → success
(bounded wait across both paths, then drop)
The retry limit is per batch, not per sendmsg_x call, ensuring the
total wait is bounded regardless of partial successes between
retries.
Dropping packets is correct because the helper implements an ethernet
link between the VM and the host. Like a physical network link under
congestion, packets may be lost. TCP retransmits lost segments, and
UDP applications are expected to tolerate loss.
Bounded retries are preferable to dropping immediately because TCP
retransmission is far more expensive than a 50µs sleep. Dropping
causes TCP to halve its congestion window and wait for retransmit,
taking milliseconds to seconds to recover. In testing, dropping
immediately on ENOBUFS caused throughput to collapse from 26 Gbps to
20 Gbps with 4 parallel streams as TCP repeatedly backed off.
ENOBUFS occurs when the VM cannot drain the vmnet buffer fast enough.
With a fast VMM (krunkit) and --busy-poll, the VM processes packets
fast enough that ENOBUFS never occurs even at 36 Gbps:
krunkit, --busy-poll, 4 CPUs, 8 MiB buffer:
TX (Gbps) drops fast calls/sec
before (retry) 36.4 0 14.5k
after (drop) 36.4 0 14.5k
With a slower VMM (vfkit), the VM cannot keep up and ENOBUFS occurs.
The bounded retry mechanism absorbs most ENOBUFS pressure, but some
packets are dropped. The previous unbounded retry behavior achieved
slightly higher throughput for vfkit by naturally throttling the
sending rate:
vfkit, --busy-poll, 4 CPUs, 4 MiB buffer:
TX (Gbps) drops fast calls/sec
before (retry) 9.8 0 15-28k
after (drop) 9.4 780 ~28k
The slight throughput regression for slow VMMs is acceptable because
head-of-line blocking is a correctness issue, not a performance
issue. Without bounded retries, a single blocked batch can stall all
traffic for an unbounded duration.
Based on #266 with the following changes:
- Tighter retry budget: 10 retries (500µs) vs 100 (5ms). Benchmarks
show retries rarely exceed a few attempts.
- Change error logging in sendmsg_x() and write() from ERROR to DEBUG
for ENOBUFS (normal backpressure signal) and WARN for other errors
(unexpected but recoverable).
- Drops tracked via stats counters instead of a separate rate-limited
warning log.
Thanks: Leo Lännenmäki <leo.lannenmaki@avrea.com>
Fixes: #267
When the kernel's vmnet buffer is full, the helper retries
sendmsg_x() with a 50 microsecond sleep between attempts. Previously
the retries were unbounded, causing head-of-line blocking: a
high-priority small packet (e.g. an HTTP/2 PING or TCP ACK) queued
behind large data packets is delayed without bound while the helper
waits for buffer space. If the delay exceeds the protocol's timeout,
the connection breaks even though the network is otherwise healthy.
Limit retries to 10 attempts (500µs maximum wait) shared across the
entire batch. The fast path retries sendmsg_x() within this budget,
then falls back to the slow path which writes remaining packets one
at a time, also retrying within the same budget. Packets are dropped
only after the budget is exhausted or on non-ENOBUFS errors.
Before (ENOBUFS in fast path):
sendmsg_x(batch) → ENOBUFS
→ sleep 50µs → retry → ENOBUFS
→ sleep 50µs → retry → ...
(unbounded retries, blocks all packets in the batch)
After (ENOBUFS in fast path):
sendmsg_x(batch) → ENOBUFS
→ sleep 50µs → retry (up to 10 times, shared budget)
→ fall back to slow path
→ write(packet₁) → ENOBUFS → retry (same budget)
→ write(packet₁) → success
→ write(packet₂) → ENOBUFS → budget exhausted → drop
→ write(packet₃) → success
(bounded wait across both paths, then drop)
The retry limit is per batch, not per sendmsg_x call, ensuring the
total wait is bounded regardless of partial successes between
retries.
Dropping packets is correct because the helper implements an ethernet
link between the VM and the host. Like a physical network link under
congestion, packets may be lost. TCP retransmits lost segments, and
UDP applications are expected to tolerate loss.
Bounded retries are preferable to dropping immediately because TCP
retransmission is far more expensive than a 50µs sleep. Dropping
causes TCP to halve its congestion window and wait for retransmit,
taking milliseconds to seconds to recover. In testing, dropping
immediately on ENOBUFS caused throughput to collapse from 26 Gbps to
20 Gbps with 4 parallel streams as TCP repeatedly backed off.
ENOBUFS occurs when the VM cannot drain the vmnet buffer fast enough.
With a fast VMM (krunkit) and --busy-poll, the VM processes packets
fast enough that ENOBUFS never occurs even at 36 Gbps:
krunkit, --busy-poll, 4 CPUs, 8 MiB buffer:
TX (Gbps) drops fast calls/sec
before (retry) 36.4 0 14.5k
after (drop) 36.4 0 14.5k
With a slower VMM (vfkit), the VM cannot keep up and ENOBUFS occurs.
The bounded retry mechanism absorbs most ENOBUFS pressure, but some
packets are dropped. The previous unbounded retry behavior achieved
slightly higher throughput for vfkit by naturally throttling the
sending rate:
vfkit, --busy-poll, 4 CPUs, 4 MiB buffer:
TX (Gbps) drops fast calls/sec
before (retry) 9.8 0 15-28k
after (drop) 9.4 780 ~28k
The slight throughput regression for slow VMMs is acceptable because
head-of-line blocking is a correctness issue, not a performance
issue. Without bounded retries, a single blocked batch can stall all
traffic for an unbounded duration.
Based on #266 with the following changes:
- Tighter retry budget: 10 retries (500µs) vs 100 (5ms). Benchmarks
show retries rarely exceed a few attempts.
- Change error logging in sendmsg_x() and write() from ERROR to DEBUG
for ENOBUFS (normal backpressure signal) and WARN for other errors
(unexpected but recoverable).
- Drops tracked via stats counters instead of a separate rate-limited
warning log.
Thanks: Leo Lännenmäki <leo.lannenmaki@avrea.com>
Fixes: #267
When the kernel's vmnet buffer is full, the helper retries
sendmsg_x() with a 50 microsecond sleep between attempts. Previously
the retries were unbounded, causing head-of-line blocking: a
high-priority small packet (e.g. an HTTP/2 PING or TCP ACK) queued
behind large data packets is delayed without bound while the helper
waits for buffer space. If the delay exceeds the protocol's timeout,
the connection breaks even though the network is otherwise healthy.
Limit retries to 10 attempts (500µs maximum wait) shared across the
entire batch. The fast path retries sendmsg_x() within this budget,
then falls back to the slow path which writes remaining packets one
at a time, also retrying within the same budget. Packets are dropped
only after the budget is exhausted or on non-ENOBUFS errors.
Before (ENOBUFS in fast path):
sendmsg_x(batch) → ENOBUFS
→ sleep 50µs → retry → ENOBUFS
→ sleep 50µs → retry → ...
(unbounded retries, blocks all packets in the batch)
After (ENOBUFS in fast path):
sendmsg_x(batch) → ENOBUFS
→ sleep 50µs → retry (up to 10 times, shared budget)
→ fall back to slow path
→ write(packet₁) → ENOBUFS → retry (same budget)
→ write(packet₁) → success
→ write(packet₂) → ENOBUFS → budget exhausted → drop
→ write(packet₃) → success
(bounded wait across both paths, then drop)
The retry limit is per batch, not per sendmsg_x call, ensuring the
total wait is bounded regardless of partial successes between
retries.
Dropping packets is correct because the helper implements an ethernet
link between the VM and the host. Like a physical network link under
congestion, packets may be lost. TCP retransmits lost segments, and
UDP applications are expected to tolerate loss.
Bounded retries are preferable to dropping immediately because TCP
retransmission is far more expensive than a 50µs sleep. Dropping
causes TCP to halve its congestion window and wait for retransmit,
taking milliseconds to seconds to recover. In testing, dropping
immediately on ENOBUFS caused throughput to collapse from 26 Gbps to
20 Gbps with 4 parallel streams as TCP repeatedly backed off.
ENOBUFS occurs when the VM cannot drain the vmnet buffer fast enough.
With a fast VMM (krunkit) and --busy-poll, the VM processes packets
fast enough that ENOBUFS never occurs even at 36 Gbps:
krunkit, --busy-poll, 4 CPUs, 8 MiB buffer:
TX (Gbps) drops fast calls/sec
before (retry) 36.4 0 14.5k
after (drop) 36.4 0 14.5k
With a slower VMM (vfkit), the VM cannot keep up and ENOBUFS occurs.
The bounded retry mechanism absorbs most ENOBUFS pressure, but some
packets are dropped. The previous unbounded retry behavior achieved
slightly higher throughput for vfkit by naturally throttling the
sending rate:
vfkit, --busy-poll, 4 CPUs, 4 MiB buffer:
TX (Gbps) drops fast calls/sec
before (retry) 9.8 0 15-28k
after (drop) 9.4 780 ~28k
The slight throughput regression for slow VMMs is acceptable because
head-of-line blocking is a correctness issue, not a performance
issue. Without bounded retries, a single blocked batch can stall all
traffic for an unbounded duration.
Based on #266 with the following changes:
- Tighter retry budget: 10 retries (500µs) vs 100 (5ms). Benchmarks
show retries rarely exceed a few attempts.
- Change error logging in sendmsg_x() and write() from ERROR to DEBUG
for ENOBUFS (normal backpressure signal) and WARN for other errors
(unexpected but recoverable).
- Drops tracked via stats counters instead of a separate rate-limited
warning log.
Thanks: Leo Lännenmäki <leo.lannenmaki@avrea.com>
Fixes: #267
When the kernel's vmnet buffer is full, the helper retries
sendmsg_x() with a 50 microsecond sleep between attempts. Previously
the retries were unbounded, causing head-of-line blocking: a
high-priority small packet (e.g. an HTTP/2 PING or TCP ACK) queued
behind large data packets is delayed without bound while the helper
waits for buffer space. If the delay exceeds the protocol's timeout,
the connection breaks even though the network is otherwise healthy.
Limit retries to 10 attempts (500µs maximum wait) shared across the
entire batch. The fast path retries sendmsg_x() within this budget,
then falls back to the slow path which writes remaining packets one
at a time, also retrying within the same budget. Packets are dropped
only after the budget is exhausted or on non-ENOBUFS errors.
Before (ENOBUFS in fast path):
sendmsg_x(batch) → ENOBUFS
→ sleep 50µs → retry → ENOBUFS
→ sleep 50µs → retry → ...
(unbounded retries, blocks all packets in the batch)
After (ENOBUFS in fast path):
sendmsg_x(batch) → ENOBUFS
→ sleep 50µs → retry (up to 10 times, shared budget)
→ fall back to slow path
→ write(packet₁) → ENOBUFS → retry (same budget)
→ write(packet₁) → success
→ write(packet₂) → ENOBUFS → budget exhausted → drop
→ write(packet₃) → success
(bounded wait across both paths, then drop)
The retry limit is per batch, not per sendmsg_x call, ensuring the
total wait is bounded regardless of partial successes between
retries.
Dropping packets is correct because the helper implements an ethernet
link between the VM and the host. Like a physical network link under
congestion, packets may be lost. TCP retransmits lost segments, and
UDP applications are expected to tolerate loss.
Bounded retries are preferable to dropping immediately because TCP
retransmission is far more expensive than a 50µs sleep. Dropping
causes TCP to halve its congestion window and wait for retransmit,
taking milliseconds to seconds to recover. In testing, dropping
immediately on ENOBUFS caused throughput to collapse from 26 Gbps to
20 Gbps with 4 parallel streams as TCP repeatedly backed off.
ENOBUFS occurs when the VM cannot drain the vmnet buffer fast enough.
With a fast VMM (krunkit) and --busy-poll, the VM processes packets
fast enough that ENOBUFS never occurs even at 36 Gbps:
krunkit, --busy-poll, 4 CPUs, 8 MiB buffer:
TX (Gbps) drops fast calls/sec
before (retry) 36.4 0 14.5k
after (drop) 36.4 0 14.5k
With a slower VMM (vfkit), the VM cannot keep up and ENOBUFS occurs.
The bounded retry mechanism absorbs most ENOBUFS pressure, but some
packets are dropped. The previous unbounded retry behavior achieved
slightly higher throughput for vfkit by naturally throttling the
sending rate:
vfkit, --busy-poll, 4 CPUs, 4 MiB buffer:
TX (Gbps) drops fast calls/sec
before (retry) 9.8 0 15-28k
after (drop) 9.4 780 ~28k
The slight throughput regression for slow VMMs is acceptable because
head-of-line blocking is a correctness issue, not a performance
issue. Without bounded retries, a single blocked batch can stall all
traffic for an unbounded duration.
Based on #266 with the following changes:
- Tighter retry budget: 10 retries (500µs) vs 100 (5ms). Benchmarks
show retries rarely exceed a few attempts.
- Change error logging in sendmsg_x() and write() from ERROR to DEBUG
for ENOBUFS (normal backpressure signal) and WARN for other errors
(unexpected but recoverable).
- Drops tracked via stats counters instead of a separate rate-limited
warning log.
Thanks: Leo Lännenmäki <leo.lannenmaki@avrea.com>
Fixes: #267
|
Fixed in #278 |
When sendmsg_x() or write() fail with ENOBUFS, write_to_vm() retries every
50 microseconds with no bound. The vm socket is a datagram socket, so ENOBUFS
means the peer receive buffer is full: the client (vfkit, krunkit) is not
draining the socket as fast as the host sends. A client that stops reading,
or reads too slowly, blocks write_to_vm() until the client catches up.
All host->vm forwarding runs on one serial queue, so one slow client delays
every host->vm packet behind it, for all flows on the interface, including
latency sensitive frames such as TCP acks and HTTP/2 pings.
A stalled client also prevents the helper from stopping: on SIGTERM the
atexit handler stops the vmnet interface on the same blocked queue, so the
helper never exits and has to be killed.
We hit this with vfkit VMs. During large concurrent downloads into a guest,
the guest drains slowly and the helper holds the queue full for seconds at a time.
HTTP/2 clients inside the guest time out their connection health pings and
close the connection, killing all in-flight streams with "http2: client connection lost".
Fix
Bound the wait to VM_MAX_RETRIES (5 ms), shared by the whole batch. When the
budget is exhausted every remaining packet gets one last write() before being dropped,
since a smaller packet may still fit in the peer receive buffer. Dropping is safe: ethernet
does not guarantee delivery, the guest transport protocols recover from the loss, and
TCP backs off, which keeps the queue short. Dropped packets are counted and logged
at most once per second.
Example
The first commit adds a stalled client test: it fills its own receive buffer by sending pings
to the gateway without reading the replies, waits for the dropped packets warning in the
helper log, and verifies that forwarding recovers once the client reads again. It fails
without the fix, since the helper never gives up and blocks until the client reads,
and passes with it.
Benchmarked with ./bench (vfkit, shared mode, iperf3): no measurable change.
The bound is never reached at line rate and no packets were dropped.
AI Disclaimer
As you can probably see from the description text above, LLMs were used
in generating this PR description. They were also used in debugging the problem in
the first place and in coming up with the failing test and actual fix.
I'm not that interested getting this exact PR merged as I'm in letting the maintainers
know of the issue we ran into and the, AI generated, workaround we found in
hopes of getting some fix landed.
Happy to provide more debugging assistance if need be.