Keeps what a destructive post-mortem drain returned until someone has observed it.
Android's ApplicationExitInfo drain (Mob.PostMortem.Android) advances an
on-disk marker when it returns an exit, and iOS MetricKit
(Mob.PostMortem.IOS) hands a payload over once. Either way the only copy
left is the capsule on this boot's in-memory Mob.Defect.Bus. If the boot
dies before anyone looks — mix mob.connect restarts the app, for one — the
exit is gone from every surface.
So each sweep writes what it drained to mob_post_mortem_journal.etf in Mob.data_dir/0
before emitting it, and re-emits every entry still in the journal on
every sweep, including the first sweep of the next boot. Within one boot,
Mob.PostMortem.Registry.emit_once/2 keeps a re-sweep from emitting an entry
twice.
Observed
An entry leaves the journal once its own capsule has been seen:
- the emit handed it to at least one
Mob.Defect.Bussubscriber, or Mob.Defect.Bus.recent/1returned it.
Nothing else counts. classes/1 shows a class row, not the occurrence, and
subscribing, or a later capsule reaching a subscriber, says nothing about a
capsule emitted before. A recent/1 that runs between a sweep's emit and its
bookkeeping is missed, and the entry is emitted again by the next boot: the
journal errs toward a duplicate, never toward a loss.
Bounded and never fatal
The journal holds at most 32 entries. Past that the oldest are dropped
and counted (read/1's dropped), and a warning is logged; a sweep still
emits everything it drained. A journal that cannot be decoded is treated as
empty, logged, and overwritten by the same sweep, so the warning appears
once. Writes go to a temporary file that is synced and renamed over the
journal.
Every read-modify-write of the file runs in one node-local process,
registered as Mob.PostMortem.Journal, started on first use and unlinked.
It also holds which of this boot's capsules are still waiting to be
observed; a :persistent_term flag, set only while that set is non-empty,
is all Mob.Defect.Bus.recent/1 reads before deciding to call it. It is
called only by sweeps, and by recent/1 while entries are waiting; never by
an emit. The file is the record, so the process dying loses no evidence: a
caller it was serving logs and carries on (a sweep still emits), the next
caller starts a new one, and entries whose observation it can no longer
match are emitted again by the next boot. Nothing here raises into a sweep
or a bus reader.
Summary
Functions
Returns a specification to start this module under a supervisor.
The journal's path on this device: mob_post_mortem_journal.etf in Mob.data_dir/0.
Clear the journaled entries whose capsules are among capsules.
What the journal at path (default: default_path/0) holds: the entries
still waiting to be observed, oldest first, and how many were ever dropped
past the 32-entry bound.
Journal fresh ({id, entry} pairs drained from source), then emit every
entry of source still in the journal followed by the fresh ones, each
through Registry.emit_once/2 as the capsule build makes of it.
Types
Functions
Returns a specification to start this module under a supervisor.
See Supervisor.
@spec default_path() :: Path.t()
The journal's path on this device: mob_post_mortem_journal.etf in Mob.data_dir/0.
@spec observed([Mob.Defect.Capsule.t()]) :: :ok
Clear the journaled entries whose capsules are among capsules.
Mob.Defect.Bus.recent/1 calls this with what it returns. Unless this boot
emitted journaled entries nobody has observed yet, it is one
:persistent_term read. Never raises.
@spec read(Path.t()) :: %{entries: [entry()], dropped: non_neg_integer()}
What the journal at path (default: default_path/0) holds: the entries
still waiting to be observed, oldest first, and how many were ever dropped
past the 32-entry bound.
@spec sweep((-> Path.t()), atom(), [{String.t(), map()}], (map() -> Mob.Defect.Capsule.t())) :: [ Mob.Defect.Capsule.t() ]
Journal fresh ({id, entry} pairs drained from source), then emit every
entry of source still in the journal followed by the fresh ones, each
through Registry.emit_once/2 as the capsule build makes of it.
An emitted capsule that reached a subscriber is cleared at once; the rest
wait for observed/1. path is resolved lazily, so a data directory that
cannot be resolved costs the journal, not the sweep. Returns the capsules
emitted.