Skip to content

pof: fix conditional delay buffer stalling under sustained reordered traffic - #1

Open
howhangliu wants to merge 4 commits into
EricssonResearch:mainfrom
howhangliu:pof-deadline-fix-frer-testbed
Open

pof: fix conditional delay buffer stalling under sustained reordered traffic#1
howhangliu wants to merge 4 commits into
EricssonResearch:mainfrom
howhangliu:pof-deadline-fix-frer-testbed

Conversation

@howhangliu

Copy link
Copy Markdown

Summary

Fixes three defects in the POF (Packet Ordering Function) worker that
cause the conditional delay buffer to fill permanently and drop all
subsequent packets under sustained, reordered traffic.

Adds a test/pof/pof.py case that fails on main and passes here, plus
the network-namespace testbed the bug was originally found on.

The bug

Observed with a FRER stream at 100 pkt/s replicated over two member
paths with independent delay (reordering between the members). After a
short time DNT logs

buffer is full, dropping new packet

and never recovers — every subsequent packet on the stream is dropped.

1. Expired deadline re-arms to take_any_time instead of firing

In pof_thread(), the poll timeout was computed as:

if (next_deadline && timespeccmp(next_deadline, &now, >))
    timespecsub(next_deadline, &now, &timeout);
else
    timeout = pof->pof_take_any_time;

The else branch conflates two different cases: no deadline at all
and deadline already in the past. When the earliest forward deadline
had already passed, the thread armed take_any_time (seconds) rather
than firing immediately. Under continuous traffic each new arrival
re-armed it again, so held packets were never released, the buffer
filled, and everything after that was dropped.

Split the cases: NULL deadline keeps take_any_time, a deadline in
the past arms a zero timeout.

2. eventfd counter values tested as bit flags

eventfd accumulates: two concurrent POF_IN_ORDER_PKT (1) writes read
back as 2, which is POF_OUT_OF_ORDER_PKT. Testing bits of the sum
therefore misclassifies events and drops in-order wakeups.

Attempt a forward on any positive event; pof_try_forward() already
only forwards when the head is next in order, so this is harmless.

3. get_next_deadline() did not select the earliest deadline

The scan used timespeccmp(..., !=) against a moving target and started
at q_head, so it ended up tracking the last differing element rather
than the minimum, and left next_to_forward unset in the single-element
case. Compare with < and seed the scan properly.

Additionally, pof_try_forward() dereferenced pof->q_head
unconditionally; it can be NULL when an event is read after a timeout
already drained the queue. Guarded.

Reproduction

test/pof/pof.py gains a case that fails on main and passes with this
branch
:

# main                          # this branch
... 6/7 successfully            ... 7/7 successfully
Test POF expired deadline ... X Test POF expired deadline ... OK
[POF] [WARNING] buffer is full, dropping new packet.   (x6)

Why the existing cases miss it

existing cases new case
MaxDelay 520 ms 16 ms
TakeAnyTime 1000 ms 5000 ms
BufferSize 200 32
packets 100 300

Two independent reasons the current suite cannot reach the bug: each case
sends 100 packets into a BufferSize = 200 queue, so buffer is full is
arithmetically unreachable; and MaxDelay = 520 ms sits far above the
30 ms path delay, so a forward deadline essentially never expires while
packets are held.

dntbr1_stall.ini inverts both, and the case drives a single member path
and briefly downs it mid-ping, leaving a sequence gap that can never be
filled - so POF must release the held head on its deadline.

The assertion is on DNT's own buffer is full, dropping new packet
warning, not on ping delivery. The deliberate link-down costs packets in
both the healthy and the stalled case, so delivered counts land only a few
packets apart (287 vs 293 observed) - not a usable signal. The warning is
logged at the default level, so no extra verbosity is needed.

The original scenario

dnt_testbed/ is where this was first hit: a FRER stream replicated over
two member paths with independent exponential delay, recovered and
reordered by a second DNT instance. It is self-contained (network
namespaces, veth, tc netem, no external hardware);
dnt_testbed/README.md has the walkthrough:

cd dnt_testbed
sudo bash -c 'source env_frer_aoi.sh && setup'
sudo ./verify_setup.sh
sudo ./run_point.sh

Notes for review

  • The testbed ships scripts and docs only; generated result files are
    gitignored. dnt_testbed/README.md is written around a specific
    study (a delay/history-length sweep), so parts of it are more
    detailed than a pure regression harness would need — happy to trim it
    down, split it into a separate PR, or move it under test/ if you
    would rather keep it out of the tree root.
  • The testbed pulls in python3-scapy and python3-regex in addition
    to what .gitlab-ci.yml already installs.
  • The new test covers the expired-deadline stall only. The eventfd
    counter fix and the q_head NULL guard have no direct coverage here.
    The NULL deref in particular is only reachable with POF:PACKET
    logging enabled, since log_packet() guards evaluation of its own
    arguments - at the default level the offending dereference is never
    executed.

howhangliu and others added 4 commits September 2, 2026 09:54
Three further fixes to the POF worker found under sustained traffic
(100 pkt/s FRER stream with reordering between member paths):

- When the earliest forward deadline had already passed at re-arm
  time, the poll timeout was set to take_any_time (seconds) instead
  of firing immediately. Under continuous traffic every new arrival
  re-armed it again, so held packets were never released: the
  conditional delay buffer filled up and all subsequent packets were
  dropped ('buffer is full, dropping new packet'). Arm a zero
  timeout when the deadline is in the past.

- The eventfd is a counter, so concurrent notifications sum up
  (e.g. two IN_ORDER writes read back as 2 == OUT_OF_ORDER) and
  bit-testing the sum misclassifies events. Attempt a forward on any
  positive event; pof_try_forward() already only forwards when the
  head is next in order.

- Guard pof_try_forward() against an empty queue: it dereferenced
  q_head unconditionally, which can be NULL when an event is read
  after a timeout already drained the queue.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds the network-namespace testbed used to find and verify the two
POF fixes in this branch. It is self-contained (netns + veth + tc
netem, no external hardware) so the failure can be reproduced from a
clean checkout.

Topology: a periodic talker feeds a FRER stream that is replicated
over two member paths with independent exponential delay, then
recovered and reordered by a second DNT instance (nxp1.ini ->
nxp2.ini). The reordering between member paths under sustained
100 pkt/s traffic is what drove the POF conditional delay buffer into
the 'buffer is full, dropping new packet' state before the fix.

Contents:
  env_frer_aoi.sh    namespace/veth/netem builder (source as root)
  verify_setup.sh    checks the environment before a run
  talker.py          periodic source
  listener.py        receiver, records per-packet peak age
  gen_expo_dist.py   netem exponential delay table
  run_point.sh       one (D,H) measurement point
  sweep.sh           sweeps D, R repetitions per point
  aggregate_results.py  sweep output -> per-(D,H) means with 95% CIs
  README.md          full walkthrough, incl. the repro procedure

Generated result files are gitignored; finished campaigns can be
archived under dnt_testbed/results_archive/ if wanted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds a case that fails on the unfixed tree and passes with the POF fixes
in this branch (6/7 vs 7/7).

The existing cases cannot reach this bug: each sends 100 packets into a
BufferSize=200 queue, so the conditional delay buffer can never fill, and
MaxDelay=520 sits far above the 30ms path delay, so a forward deadline
essentially never expires while packets are held.

dntbr1_stall.ini inverts both: MaxDelay=16 is below the path delay so
deadlines do expire, BufferSize=32 is well under the packet count, and
TakeAnyTime=5000 is large enough that take-any cannot mask the stall
within the run. The test drives a single member path and briefly downs it
mid-ping, leaving a sequence gap that can never be filled, so POF has to
release the held head on its deadline.

The assertion is on DNT's own 'buffer is full, dropping new packet'
warning rather than on ping delivery: the deliberate link-down costs
packets in both the healthy and the stalled case, leaving the delivered
counts only a few packets apart (287 vs 293 observed), which is not a
usable signal. The warning is logged at the default level, so the test
needs no extra verbosity.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant