Skip to content

Reconnect: node-swap race when 2+ pads disconnect at once and reclaim each other's freed eventN #151

Description

@aradanmn

Edge case found on-Deck (2026-07-26) right after #38 shipped. Single-pad reconnect is solid; this is the multi-pad-simultaneous-swap corner.

Repro

4-up, proxy on. Disconnect P4 then P2 (both down). Reconnect P4 then P2. Observed: P4's pad drives P2's instance, and P4's instance is uncontrollable.

The identity logic is CORRECT

The orchestrator matched by MAC perfectly despite the swap:

REMOVE event30 → slot 4    REMOVE event22 → slot 2
ADD event22 MAC=90:89 → RESUME slot 4   (P4 came back on P2's old node)
ADD event30 MAC=dc:0c → RESUME slot 2   (P2 came back on P4's old node)

Final state + symlinks are right: slot4→event22, slot2→event30.

Root cause — a race between the repoint and evsieve's own persist=reopen

When P4's pad reconnected on event22 (P2's old node), evsieve-slot2 was still waiting to reopen event22 (its symlink not yet repointed), so it grabbed P4's pad first (exclusive grab). By the time the orchestrator repointed slot4→event22, evsieve-slot4 can't grab it:

slot4 evsieve: failed to grab input device: libevdev status code -16 (EBUSY) — grabbed by another program
slot2 evsieve: "capabilities of the reconnected device are different than expected"

evsieve selects its input by symlink path, and it can't filter by uniq (DS4s share vendor:product), so on node-number reuse the wrong slot's evsieve wins the grab.

Fix directions

  1. Quiesce-then-repoint (preferred): when handling a reconnect batch, first point every affected slot's pads-symlink at a dead/nonexistent node (releasing stale grabs), then repoint each to the correct node by MAC. Removes the window where a stale symlink grabs the wrong pad.
  2. Detect the swap (target node already grabbed by another slot's evsieve) → stop that evsieve, repoint, restart.
  3. Debounce: process a burst of REMOVE/ADD together rather than one-at-a-time so repoints land before evsieve's next persist poll.

Severity

Low — needs ≥2 pads dropped simultaneously AND reconnecting into each other's freed numbers. Single-pad reconnect (the dominant real case, e.g. one battery dies) works. Recovery today: restart the affected instance(s).

Refs #38.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions