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, this recorder detects the enclosing transaction and warns that the guarantee is suspended for that call.
Reeve::Audit::IsolatedRecorder is the answer where the database has concurrent
writers: same rows, same redaction, on a pool of its own, so a rollback on the
host's connection cannot reach it. It is opt-in rather than the default precisely
because it cannot work everywhere this one does. A host with a different answer
again — a queue, an append-only log — still supplies its own audit_recorder.
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.
Direct Known Subclasses
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
46 47 48 |
# File 'lib/reeve/audit/recorder.rb', line 46 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.
57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 |
# File 'lib/reeve/audit/recorder.rb', line 57 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 |