Argus.Extractors.Reply (Panoptes v0.13.0)

Copy Markdown View Source

What an OTP callback returns, and whether it kept the means to reply.

handle_call/3 may answer immediately with {:reply, value, state}, or defer by returning {:noreply, state} and calling GenServer.reply/2 later. Deferring is a promise, and the only thing that can discharge it is the from term the callback was handed — an opaque {pid, tag} that exists nowhere else.

So a handle_call/3 clause that returns {:noreply, _} while never reading its from parameter has made a promise it cannot keep, no matter what the rest of the system does. The caller blocks for the full GenServer.call/3 timeout and then exits.

Approach

Both halves are visible in bytecode without dataflow.

A callback's return shape is a tuple built into {x, 0} immediately before return, and its tag is a literal atom:

{:put_tuple2, {:x, 0}, {:list, [atom: :noreply, x: 2]}}
:return

And from is argument 1, so it arrives in {x, 1}. Reading it is either a mention of that register or a call of arity two or more, because calls take their arguments positionally: a body that passes from straight through, as publish(payload, from, state) does, compiles to no move at all and mentions the register nowhere.

Which sites drop from is then a reachability question — walk the control-flow graph from the entry, refuse to pass through any instruction that reads from, and see which {:noreply, _} sites remain. Per site, not per function: handle_call/3 compiles every clause into one function, and asking function-wide lets a clause that defers correctly vouch for one that does not.

Both directions of imprecision are toward silence. A tail call hides the return shape, so no tag is recorded; a write to {x, 1} counts as a read, so a clobbered register looks retained; and any two-argument call counts, so a callback that merely passes its arguments along is never reported.

Emitted facts

  • callback_return(id, func, callback, tag) — a literal return tag
  • callback_stop_reason(id, func, reason) — the reason of a {:stop, reason, ...} return when it is a literal atom or a {:shutdown, term} literal
  • callback_timeout(id, func, callback, timeout_ms) — the literal integer timeout of a {:ok, state, ms}, {:noreply, state, ms} or {:reply, reply, state, ms} return
  • callback_drops_from(id, func) — a {:noreply, _} site some execution reaches without ever having read from

A whole return folded into one literal — {:ok, %{}, 0} with a constant state compiles to a single move of the tuple into {x, 0} — is read the same way as a tuple built in place.