lean4-htt/tests/server_interactive/cancellation_empty_by.lean
Joachim Breitner 0c4d2648b5
test: harden cancellation_empty_by against scheduling races (#13704)
This PR fixes issues with flaky cancellation_empty_by test.

The original test gated the runner's `waitFor: blocked` on `t1`'s
`wait_for_cancel_once_async`, which had no causal relationship to
`tracerSuggestion` having actually run inside the empty-`by` snapshot
task. On CI under load the runner could trigger the insert before
`tracerSuggestion` registered its `onSet` callback, leading to
intermittent timeouts.

Add a label-keyed `IO.Promise` registry (`syncPromisesRef`) plus
`getSyncPromise` / `resolveSyncPromise` primitives and a
`wait_for_sync <label>` tactic to `Lean.Server.Test.Cancel`. The
empty-`by` test's `tracerSuggestion` now resolves a sync promise
after registering its onSet, and `t1` waits on that promise before
emitting `blocked`. The empty-`by` example must precede `t1` because
`try?` inside the snapshot task synchronously waits on prior pending
async theorem bodies during library search (likely a separate upstream
issue); with that ordering the test is fully deterministic.

`t1` also drops `wait_for_cancel_once_async` in favor of plain
`trace "blocked"`. The test now also `dbg_trace`s at each sync point
(`tracerSuggestion ready`, `sync received`, `cancelTokenSet`) so the
.out.expected captures the deterministic execution sequence.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 10:34:38 +00:00

73 lines
2.5 KiB
Text

import Lean.Server.Test.Cancel
import Lean.Elab.Tactic.Try
open Lean Lean.Meta Lean.Elab Lean.Elab.Term Lean.Elab.Tactic
open Lean.Server.Test.Cancel
/-!
Test that `cancelRec` reaches the snapshot task spawned by
`elabEmptyByAsTry` on re-elaboration.
Chronological flow:
1. Empty-`by` example elaborates; `elabEmptyByAsTry` spawns a snapshot
task with its own cancel token and returns.
2. The snapshot task's `try?` calls `tracerSuggestion`, which:
- registers test task `"cancelTokenSet"`,
- registers `cancelTk.onSet` to resolve it,
- resolves sync promise `"tracerSuggestion"`,
- returns `wait_for_test_task "cancelTokenSet"` as the candidate.
3. `try?` evaluates the candidate; it blocks on the test task.
4. `t1` elaborates: `wait_for_sync "tracerSuggestion"` returns
immediately, `trace "blocked"` emits the diagnostic.
5. Runner inserts `; skip`, triggers re-elab; `cancelRec` sets the
cancel token; `onSet` resolves the test task; the candidate's wait
returns; the snapshot task body completes.
Failure modes:
- `cancelTk? := none` to `Core.logSnapshotTask`: `cancelRec` cannot
reach the cancel token, the wait blocks, runner times out.
- `cancelTk? := none` to `wrapAsyncAsSnapshot`: `tracerSuggestion`
sees no cancel token, doesn't register `onSet`; the promise drops;
`wait_for_test_task` surfaces a `task dropped` diagnostic.
Ordering note: the empty-`by` example must precede `t1` because `try?`
inside the snapshot task library-searches the environment in a way
that synchronously waits on prior pending async theorem bodies. With
`t1` first, its `wait_for_sync` would block the snapshot's `try?`,
deadlocking. Likely a separate upstream issue.
-/
namespace TestEmptyBy
opaque UnsolvableProp : Prop
@[try_suggestion]
def tracerSuggestion (_goal : MVarId) (_info : Try.Info) :
MetaM (Array (TSyntax `tactic)) := do
if let some prom ← mkTestTask "cancelTokenSet" then
if let some cancelTk := (← readThe Core.Context).cancelTk? then
cancelTk.onSet (do
dbg_trace "cancelTokenSet"
prom.resolve ())
dbg_trace "tracerSuggestion ready"
resolveSyncPromise "tracerSuggestion"
return #[← `(tactic| wait_for_test_task "cancelTokenSet")]
end TestEmptyBy
set_option tactic.tryOnEmptyBy true
example : True := by
trivial
--^ waitFor: blocked
--^ insert: "; skip"
--^ sync
example : TestEmptyBy.UnsolvableProp := by
theorem t1 : True := by
wait_for_sync "tracerSuggestion"
dbg_trace "sync received"
trace "blocked"
trivial