Skip to content

Observed tests: wait for the restarted collector's init before reading it - #80

Merged
HackTuah merged 1 commit into
mainfrom
fix/restart-gap-flake
Oct 7, 2026
Merged

HackTuah merged 1 commit into
mainfrom
fix/restart-gap-flake

Conversation

@HackTuah

Copy link
Copy Markdown
Member

The floor leg failed once on #78's PR run ("a call during the restart gap is answered..."): snapshot/1 answered {:error, :not_started} right after the test found the restarted pid. GenServer registers the name before init/1 runs, and init/1 creates the table snapshot/1 reads, so the test could read in between.

Both restart tests now call the new process (:sys.get_state/1, answered only after init returns) before reading it. Evidence: with a 200 ms sleep forced into init/1, the old tests fail (both; the kill test had the same latent window) and the new ones pass; unmodified, 30 seeds, 0 failures. Test-only; local gate passes.

@HackTuah
HackTuah requested a review from minitru September 26, 2026 19:31
…g it (G-090)

The floor leg failed once in "a call during the restart gap is answered...": snapshot/1
answered {:error, :not_started} after the loop had found the restarted pid. GenServer
registers the name before init/1 runs, and init/1 creates the table snapshot/1 reads, so a
poll on the name can land between the two; the floor runner is slow enough to. Both restart
tests now make a call to the new process (:sys.get_state/1, answered only after init returns)
before reading it. Measured: with a 200 ms sleep put into init/1 before the table, the old
tests fail (both of them: the kill test had the same window, latent) and the new ones pass;
unmodified, 30 seeds, 0 failures. No package change: the gap is the collector's documented
restart gap, and it ends when init returns.

Signed-off-by: Ayla Croft <aylacroft@proton.me>
@HackTuah
HackTuah force-pushed the fix/restart-gap-flake branch from b3d8436 to 47dd2e1 Compare October 7, 2026 23:32
@HackTuah
HackTuah merged commit 35b028a into main Oct 7, 2026
15 checks passed

@minitru minitru left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Post-merge review (+8 lines, one test file). The fix is correct for the flake it targets, but the second restart test still has the same race in the opposite direction. I read the diff, the merged test file and observed.ex; I did not run the suite.

What it fixes, and why it's right

  • GenServer registers the name before init/1 runs, and init/1 creates the ETS table snapshot/1 reads. The tests polled Process.whereis and could read in between.
  • :sys.get_state(new) is a sound barrier: a system message is only answered after init returns. It's better than a sleep or a retry loop on snapshot.
  • It's test-only; snapshot/1 returning {:error, :not_started} in that window matches its documented contract, so no production change is needed.

Main finding: the "gap" assertions can lose the race the other way

In the second test, right after assert_receive {:DOWN, ...}, the test asserts:

  • Observed.snapshot(name) == {:error, :not_started}
  • {Observed, name} is not in the telemetry handler list

Both assume the supervisor has not restarted the child yet. Nothing holds it back: the supervisor gets its exit signal at about the same time the test gets :DOWN, and the new init is only a table create plus a detach and attach. If the restart lands first, snapshot returns {:ok, ...} and the handler is attached again, so both assertions fail.

The PR's evidence doesn't cover this direction. The forced 200 ms sleep in init widens the gap, which makes this failure less likely, and 30 clean seeds on an idle machine say little about a loaded CI runner, which is where the first flake showed up.

A deterministic fix is to hold the supervisor still for the gap: :sys.suspend(sup) before the kill, run the gap assertions, then :sys.resume(sup) and wait for the restart as now.

Minor

  • The same four-line comment and call are pasted in both tests. A small helper (for example await_restart(name, old_pid) that does the polling and the get_state) would give the explanation one home.
  • If the restarted process dies between whereis and get_state, the test exits rather than fails an assertion. That's acceptable, just noisier to read in CI.

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.

2 participants