Skip to content

cell M6: a handover arrival placed on a position a staged schedule gives another UE - #576

Merged
josephnef merged 1 commit into
masterfrom
fix/m6-ho-position-race
Nov 14, 2026
Merged

josephnef merged 1 commit into
masterfrom
fix/m6-ho-position-race

Conversation

@josephnef

@josephnef josephnef commented Nov 14, 2026 •

Copy link
Copy Markdown
Collaborator

#566, causes not established: the m6_daps_dl run with DL to the arriving UE ~111 µs late and 13 uplinks lost, recorded as "not reproduced".

The cause, from the original logs (regf-dl/fault-daps-1):

  1. The CU's coordination schedule moved DU2's own UE from position 1 to 2. It was staged at net sf ~404, effective at 416.
  2. Right after, the CU issued the handover with target position 2. It had taken that position as free from DU2's report, which predated the schedule.
  3. DU2 accepted the command. Its free-position check read f1_view.ues, a map snapshot refreshed once per main-loop pass, not at the commit.
  4. The arrival was published at position 2 as well. The arrival's DL frame then queued behind the other UE's frame in the same slot (~110 µs, one frame's airtime), and their uplinks collided.

It was not rare. Across the saved rig logs:

  • 201 of 1550 handover commands (13 %) were refused by the target as invalid. That is the same race, caught when the target's view was fresh; the CU retried.
  • 41–53 % of the same-channel A3 handover runs had one.
  • 14 DU logs show the race missed: two UEs on one position.
  • Cross-channel runs never had it, because coordination moves only co-channel positions.

Fix

  • CU:
    • No handover while either DU has a schedule pushed and not yet applied. This is the reverse of the schedule's own in-flight guard.
    • The target id and position are chosen against the CU's own last schedule for that DU, as well as against the report.
  • DU:
    • A staged schedule's positions count as taken (Stager::staged()).
    • The map view the handover check reads is refreshed at the commit.

Rig after (fixed build on all three boxes):

  • m6_handover.sh 6 normal + 3 fault, plus m6_daps_dl.sh (all 16 runs): 25 runs, 29 commands.
  • 0 refused, no position shared.
  • The arrival's DL late_us p50 was 2–3 µs in every run.

ctest: 95/95 pass. docs/cell-m6.md "Bugs the rig found" records it.

🤖 Generated with Claude Code

…ves another UE (#566)

The 'not reproduced' m6_daps_dl run (arrival DL ~111 us late, 13 uplinks
lost) was a race: the CU's coordination schedule moved the target's own
UE onto the position the CU then gave the arrival (from a report that
predated the schedule), and the target's free-position check read a map
snapshot refreshed once per pass. 13 % of handover commands on the rig
were refused for it (the race caught); 14 DU logs show it missed.

CU: no handover while either DU has a schedule pushed and not applied;
the target id/position chosen against its own last schedule too.
DU: a staged schedule's positions are taken; the check's view of the map
refreshed at the commit.

Rig after: 25 runs, 29 commands, 0 refused, no shared position.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Prevent handover arrivals from sharing staged schedule positions

🐞 Bug fix 📝 Documentation 🕐 20-40 Minutes

Grey Divider

AI Description

• Delay handovers while either DU has an unapplied schedule, preventing selection from outdated
 reports.
• Reserve reported and staged positions on both CU and DU, and refresh the DU view at map commit.
• Document the race, its observed frequency, and post-fix rig results.
Diagram

sequenceDiagram
    participant CU as Controller
    participant Source as Source DU
    participant Target as Target DU
    participant Stager as Schedule stager
    participant Map as Map view
    Target->>CU: Report positions and versions
    Source->>CU: Report applied version
    CU->>CU: Wait for both schedules to apply
    CU->>CU: Reserve report and scheduled positions
    CU->>Target: Handover command
    Target->>Stager: Read staged positions
    Target->>Map: Read committed positions
    Target->>Target: Validate against both sets
    Map->>Map: Refresh at map commit
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Target-side position reservation handshake
  • ➕ Makes the target authoritative before the controller commits to an arrival position.
  • ➖ Requires additional protocol state and round trips, complicating handover timing.

Recommendation: Keep the layered guard and validation in this PR. It uses existing schedule versions and target-side state to prevent the collision without introducing a new reservation protocol.

Files changed (4) +57 / -2

Bug fix (3) +27 / -2
main.cppValidate arrivals against staged and committed DU positions +10/-0

Validate arrivals against staged and committed DU positions

• Includes staged schedule assignments in the handover admission check. Refreshes the shared position view when a grant map commits rather than waiting for the next main-loop pass.

examples/cell/main.cpp

F1.hExpose the DU's staged schedule for admission checks +3/-0

Expose the DU's staged schedule for admission checks

• Adds a read-only accessor so handover validation can treat positions in an unpublished schedule as occupied.

src/cell/F1.h

main.cppAvoid handovers across pending schedules and reserve planned positions +14/-2

Avoid handovers across pending schedules and reserve planned positions

• Blocks handover issuance while either DU has a pushed, unapplied schedule. Selects the target UE ID and position against both the target report and the controller's last schedule.

tools/ran-controller/main.cpp

Documentation (1) +30 / -0
cell-m6.mdDocument the staged-schedule handover race +30/-0

Document the staged-schedule handover race

• Explains how an outdated target report and stale DU map view allowed two UEs to share a position. Records observed rig frequency, the fix, and post-fix run results.

docs/cell-m6.md

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0)

Grey Divider

Great, no issues found!

Qodo reviewed your code and found no material issues that require review

Grey Divider

Tip of the day
💡 Did you know, you can ask Qodo to dismiss a finding you disagree with, with your reason on record

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

@josephnef josephnef added the skip-qodo-gate Bypass the Qodo review gate (outage / maintainer decision) label Nov 14, 2026
@josephnef
josephnef merged commit afaad61 into master Nov 14, 2026
30 of 32 checks passed
@josephnef
josephnef deleted the fix/m6-ho-position-race branch November 14, 2026 09:40
josephnef added a commit that referenced this pull request Nov 14, 2026
… the roam's slow return a stale station (#577)

#566, *causes not established*: the two that remained after #576.

**Witness logging:** each `cell.w` frame line now carries per-path RSSI
(dBm) and SNR (dB). This makes the witness a second receiver, aligned to
each DU's clock, for TX power sweeps.

**The power-to-RSSI non-linearity is one radio.** testrig
`scripts/m6_txpwr_sweep.sh` steps both DUs from 0 to −20 dB of offset,
reading at four TX→RX pairs; `results/2026-11-14-m6-causes.txt` has the
tables.
- **DU2 (j6412-3 09:00)** is linear over the full 20 dB at every
reading, with SNR 33–41 dB.
- **DU1 (j6412-1 09:00)** is linear down to 9 dBm commanded. At 8 dBm it
steps down 3–10 dB, depending on the receiving path, and the receivers'
SNR falls 13–15 dB with it. So the signal fell, not just its estimate. A
0.5 dB sweep repeats the step.
- **With the roles swapped,** j6412-1 09:00 as the receiver sees the
same collapse below about −62 dBm, from both transmitters. Links without
it read exactly linear (−35 → −55 dBm and −43 → −64.5 dBm).

That localises it to that unit, in both directions. The mechanism inside
it is not established. The crossing's "−9 dB reads −12" came from
ramping DU1 into that region.

**The roam baseline's 5.6–5.9 s return is a stale station.** testrig
`m6_roam_baseline.sh --debug 1`, A/B with `--drop-left`:
- An SME roam leaves the old AP holding the client.
- On the return, hostapd first deletes that station, and `DEL_STATION`
took 428–443 ms on every removal.
- The client's authentication wait ran out first. It fell back to the AP
it was leaving, which paid the same deletion, and ignored both for ~4.5
s.
- With the old AP dropping the client as it leaves (`deauthenticate STA
tx=0`), every roam took 0.41–0.57 s (10/10, against 5/5 slow returns
without).

`docs/cell-m6.md` updates both notes.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

skip-qodo-gate Bypass the Qodo review gate (outage / maintainer decision)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant