Mob.PostMortem.Journal (mob v0.9.7)

Copy Markdown View Source

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:

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

Types

A drained entry: its source, its artifact id, and the map the NIF returned.

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

entry()

@type entry() :: {source :: atom(), id :: String.t(), map()}

A drained entry: its source, its artifact id, and the map the NIF returned.

Functions

child_spec(init_arg)

Returns a specification to start this module under a supervisor.

See Supervisor.

default_path()

@spec default_path() :: Path.t()

The journal's path on this device: mob_post_mortem_journal.etf in Mob.data_dir/0.

observed(capsules)

@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.

read(path \\ default_path())

@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.

sweep(path, source, fresh, build)

@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.