Kino.Qx.Run (Kino.Qx v0.3.0)

Copy Markdown View Source

Implementation of Kino.Qx.run/2,3 and Kino.Qx.run!/2,3.

Wraps Qx.Hardware.run/3 with a live Kino.Frame status panel (rendered above the eventual result) and a best-effort cancel watcher that fires Qx.Hardware.cancel/3 if the caller cell process dies during a run.

Architecture

caller cell process  (Process.flag(:trap_exit, true))
  
   frame = Kino.Frame.new() |> tap(&Kino.render/1)
  
   watcher = spawn(...)                   unlinked
      monitors caller; only fires Qx.Hardware.cancel/3
         on the UNTRAPPABLE :kill path (see below)
  
   worker = Task.async(fn -> Hardware.run(...) end)
      on_status sends {:status, event} back to the caller
  
   run_loop/1  (caller stays alive in a receive loop)
         {:status, event}     frame + job_id + caller cb
         {ref, result}        render terminal, :done, return
         {:DOWN, ref, ...}    worker crash, propagate
         {:EXIT, _, reason}   trappable interrupt (below)

Interrupt semantics

Livebook's "Stop" may deliver :shutdown (trappable) or :kill (untrappable). The blocking hardware call runs in a worker Task so the caller stays in run_loop/1:

  • :shutdown (trappable) — the caller receives {:EXIT, _, :shutdown}, brutally stops the worker, issues the cancel once itself, signals the watcher :done (so the watcher stands down and does NOT cancel again on the caller's subsequent :DOWN), then raises Kino.Qx.Interrupted with the last-seen job_id. This is the now-true contract.

  • :kill (untrappable) — the caller dies immediately without running its handler. Only here does the unlinked watcher fire the cancel (its {:DOWN, caller, reason != :normal} arm). No Kino.Qx.Interrupted is raised — the process is already gone.

Single-cancel gating

On the trappable path the caller cancels and then sends the watcher :done. Erlang orders the :done message before the monitor :DOWN generated when the caller later dies, so the watcher always processes :done first and exits without cancelling — exactly one Qx.Hardware.cancel/3 fires (the caller's). On the :kill path the caller never sends :done, so exactly one cancel fires (the watcher's).

Residual races (honest caveats)

  • Spurious cancel. If the caller is killed in the narrow window after the worker returns but before send(watcher, :done), the watcher still sees an abnormal :DOWN and issues a cancel for an already-finished job. Qx.Hardware.cancel/3 treats an IBM 404 as expected, so this is harmless noise.
  • Untrappable teardown. A :kill (or a full Livebook session teardown that also kills the watcher) leaves the IBM job orphaned — it runs to completion and burns shots. This is the silent leak flagged in the plan's Risks section; only the upstream-side fix (job TTL / server cancel) fully closes it.

Summary

Functions

run(circuit, config, opts \\ [])

@spec run(Qx.QuantumCircuit.t(), Qx.Hardware.Config.t(), keyword()) ::
  {:ok, Qx.SimulationResult.t()} | {:error, term()}

run!(circuit, config, opts \\ [])