Class: Reeve::Audit::Recorder
- Inherits:
-
Object
- Object
- Reeve::Audit::Recorder
- Defined in:
- lib/reeve/audit/recorder.rb
Overview
The one write path into the ledger (FR-008).
The envelope calls this from an ensure block and expects it to raise on failure —
Constitution II makes a failed write a failed call unless the host has opted into a
degraded mode. Everything here is therefore synchronous: no queue, no thread, no
ActiveJob. An asynchronous ledger cannot be relied on and would make FR-012
unenforceable.
The insert runs in requires_new: true, which is what makes the trace survive a
tool body that opens a transaction and rolls it back (R5) — the case this was built
for, and the one it does solve.
What it does not do, corrected after review: requires_new is a SAVEPOINT, not
an independent transaction. If the host has already opened a transaction around
the invocation — a controller or middleware that wraps each request, or a test suite
using transactional fixtures — the savepoint is released into that transaction, and
a later rollback takes the ledger row with it. The write reports success and the
envelope has no way to learn otherwise, so the invocation returns records with no
surviving trace. The comment here previously claimed independence outright; it did
not have it.
A genuinely independent write needs a second connection, and that is not portable:
on SQLite the enclosing transaction holds the write lock, so a second connection
blocks until it times out. Rather than fail every call on the databases where
isolation is impossible, the recorder detects the enclosing transaction and warns
that the guarantee is suspended for that call. A host that needs durability under a
wrapping transaction supplies its own audit_recorder — writing to a separate
connection, a queue, or an append-only log — which is what that setting is for.
The other known limit, unchanged: on a single connection there is a narrow window where the tool's data commits and the ledger write then fails. The caller learns by exception, but the data change has already landed.
Constant Summary collapse
- DETAIL_LIMIT =
Free-form text from a policy or an exception message. Capped rather than trusted: it is the one column whose length the host does not control.
1000
Class Method Summary collapse
Instance Method Summary collapse
-
#initialize(entry_class: Entry, config: nil) ⇒ Recorder
constructor
A new instance of Recorder.
-
#record(attributes) ⇒ Object
Returns the entry.
Constructor Details
Class Method Details
.record(attributes) ⇒ Object
42 43 44 |
# File 'lib/reeve/audit/recorder.rb', line 42 def self.record(attributes) new.record(attributes) end |
Instance Method Details
#record(attributes) ⇒ Object
Returns the entry. Raises if the row could not be written, so the envelope can fail the invocation.
53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 |
# File 'lib/reeve/audit/recorder.rb', line 53 def record(attributes) row = row_for(attributes) warn_about_enclosing_transaction(row[:invocation_id]) entry_class.transaction(requires_new: true) do entry_class.create!(row) end rescue ActiveRecord::RecordNotUnique, ActiveRecord::RecordInvalid => e # A replayed invocation is the same invocation. One row per invocation_id means # a retry is a no-op insert, never a second row (contracts/audit-entry.md). existing = entry_class.find_by(invocation_id: row[:invocation_id]) raise e if existing.nil? existing end |