Skip to content

test: pin the join-adjacent stream-removal race and its real blast radius (#96) - #136

Open
pinetops wants to merge 6 commits into
mainfrom
fix/stream-removal-diffs
Open

test: pin the join-adjacent stream-removal race and its real blast radius (#96)#136
pinetops wants to merge 6 commits into
mainfrom
fix/stream-removal-diffs

Conversation

@pinetops

Copy link
Copy Markdown
Member

The #96 investigation, packaged. Does not close #96 — the bug is upstream and still live; this lands the evidence, the coverage, and the report artifact.

Findings (full detail in the issue comment)

  1. The recipe is join-adjacent timing, not diff shape: a stream removal (delete-only OR reset) landing within ~100ms of a join/rejoin sometimes never applies — the row survives in the DOM. Two shapes reproduce it: reload+rejoin (~60% under load) and removal of a post-join-inserted row.
  2. Attribution: upstream — a zero-lavash vanilla Phoenix.LiveView fixture reproduces it.
  3. The reset workaround is also affected — resets ride the same stream-tuple channel. Cross-session removals can silently fail under this timing regardless of mechanism; nothing had noticed because browser-level removal assertions had been dropped.
  4. Observation-sensitive: every capture attempt suppresses it.

What lands

  • Vanilla attribution fixture (test_vanilla_stream_fixture.ex + route) — plain LV, raw PubSub
  • Three browser removal tests: the stable plain-delete shape runs by default (restoring the removal coverage lost when the workaround landed); the two racy shapes + the vanilla repro are tagged :issue_96 (excluded by default — they fail intermittently by design; mix test --include issue_96 to investigate)
  • Pre-connect wire capture baked into the test layout — any future failure self-documents with a full trace
  • docs/issue96_repro.exs — standalone single-file repro (Mix.install + phoenix_playground, self-triggering via send_after, delivery-aware self-judging with server-logged verdicts) in exactly the shape phoenix_live_view's issue template requires. Driver validation pending; once it demonstrably fires it goes upstream verbatim.

Runtime unchanged — the reset path stays (least-bad). Re-test the tagged repros on every LiveView upgrade.

🤖 Generated with Claude Code

…dius (#96)

Investigation results, all reproducible via --include issue_96:

1. The loss recipe is join-adjacent timing, not diff shape: a stream
   diff (delete-only OR reset) broadcast within ~100ms of a
   join/rejoin sometimes never applies, while the row survives in
   the DOM. Two shapes reproduce it: reload+rejoin (variant A,
   ~60% under load) and deletion of a row the client learned via a
   post-join stream_insert (variant B).

2. ATTRIBUTION: UPSTREAM. A new vanilla Phoenix.LiveView fixture
   (zero lavash on the page, raw Phoenix.PubSub +
   stream_delete_by_dom_id) reproduces the same survival.

3. THE RESET WORKAROUND IS ALSO AFFECTED — resets ride the same
   stream-tuple channel, so the #96 mitigation only narrows the
   window, it doesn't close it. The old e2e never noticed because
   it dropped browser-level removal assertions entirely.

4. Heisenbug-grade: every instrumentation attempt (test-side
   onMessage installs, layout-baked capture) suppresses it.

Shipped: the vanilla attribution fixture + route; three browser
removal tests (the stable plain-delete shape runs by default,
restoring removal coverage; the two racy shapes + the vanilla repro
are tagged :issue_96, excluded by default); a permanent pre-connect
wire capture in the test layout for future evidence dumps; failure
paths dump the full wire trace.

Runtime unchanged (reset path kept — least-bad until the upstream
fix). Next step recorded in #96: minimal upstream report with the
runnable repro.
Mix.install + phoenix_playground, self-triggering (the LiveView
schedules its own stream_delete via Process.send_after — no PubSub,
no second process), delivery-aware self-judging (counts incoming
diff messages, so FAIL = diff arrived and the row survived, distinct
from NODIFF delivery artifacts), verdicts logged server-side per
page load. This is the artifact for the eventual upstream
phoenix_live_view report — their issue template requires exactly
this single-file shape.
phoenix_playground never exposes window.liveSocket, so the repro's
self-judging script waited forever and no VERDICT lines were ever
logged. The rework owns the layout (Mix.install + Bandit + bare
Endpoint), installs the wire capture before connect, anchors the
delete broadcast to the dead render so sweeps cross the join
handshake, and logs SUB/BCAST ordering to classify NODIFF verdicts.

Requires Plug.Parsers in the endpoint: without it dead-render
mounts see empty params and every trial silently runs the default
delay.
The node --check temp file was named by a content hash, so two
concurrent compiles of the same project (parallel test runs)
produced the same path — one process's cleanup deleted the file
out from under the other's validator, failing compilation with
'Cannot find module'. OS pid + unique_integer make the name
unique per invocation.
Ash.DataLayer.Ets.stop tears the table-owner process down
asynchronously; under load the next create can look up the dying
owner and insert into a dead table (:table_not_found). Seen as
intermittent seed failures in full-suite runs. One bounded retry
scoped to that error.
Pulls the onPatchEnd callback-registry rework (wallabidi#72), the
microtask-deferred query checker (wallabidi#73), and the
DOMContentLoaded visit default (wallabidi#78) — the driver no
longer restores stale patch hooks or does synchronous query work
inside LiveView's patch-end stack.
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.

Stream row removals sometimes don't apply in the browser (delete-only / reset diffs)

1 participant