Class: ForestAdminAgent::AuditTrail::Snapshots
- Inherits:
-
Object
- Object
- ForestAdminAgent::AuditTrail::Snapshots
- Includes:
- Recording
- Defined in:
- lib/forest_admin_agent/audit_trail/snapshots.rb
Overview
What a "before" hook leaves for the matching "after" hook: the records as they stood, the patch, and the ids of the pending rows to confirm.
Entries are keyed by the object the hook decorator hands to both contexts — the filter, or the data on a create — because taking the newest entry is wrong as soon as writes nest: an inner write that fails skips its after hook and stays on the stack, and the outer hook would then confirm the failed operation's rows as done and leave its own stranded. Both directions of that are lies.
An operation raising between the two hooks strands its entry, hence the cap; its rows stay pending in
the table, which is the truthful state for a write that may not have landed.
Constant Summary collapse
- MAX_PENDING =
ponytail: 16 deep is far past any legitimate nesting; raise it if one ever gets that far.
16
Constants included from Recording
Recording::DONE, Recording::IDENTITY, Recording::PENDING, Recording::REDACTED
Instance Method Summary collapse
-
#pop_for(key) ⇒ Object
The entry this operation left, or nothing rather than someone else's.
- #push(key, snapshot) ⇒ Object
-
#take(context, projection) ⇒ Object
The records an operation is about to touch, capped.
Methods included from Recording
#audit_safely, #correlation_key_for, #identity_of, #now, #redact
Instance Method Details
#pop_for(key) ⇒ Object
The entry this operation left, or nothing rather than someone else's.
A customization that replaced the filter or the data leaves no identity to match on: our before hook saw the replacement and the after context carries the original. With a single operation in flight that is unambiguous, so it still pairs; with several it does not guess, and the rows stay pending.
30 31 32 33 34 35 36 |
# File 'lib/forest_admin_agent/audit_trail/snapshots.rb', line 30 def pop_for(key) stack = pending index = stack.rindex { |entry| entry[:key].equal?(key) } index = 0 if index.nil? && stack.size == 1 index.nil? ? nil : stack.delete_at(index) end |
#push(key, snapshot) ⇒ Object
19 20 21 22 23 |
# File 'lib/forest_admin_agent/audit_trail/snapshots.rb', line 19 def push(key, snapshot) stack = pending stack.shift while stack.size >= MAX_PENDING stack.push(snapshot.merge(key: key)) end |
#take(context, projection) ⇒ Object
The records an operation is about to touch, capped. Reading "delete all" unbounded would materialise every matched row — and the pending/confirm protocol writes each of them twice. Truncation is logged rather than silent: an incomplete audit somebody knows about beats an OOM.
Read outside the write's transaction — hooks bracket the write as separate calls and the data layer
exposes no lock, on purpose, since it spans ActiveRecord, Mongoid, HTTP APIs. So two updates racing on
one record both snapshot the same state, and the one that lands second records a previous_values that
was already overwritten.
An empty list on failure rather than no snapshot at all: the after hook pops unconditionally, so
skipping the push would pair it with an unrelated entry. Reading it goes through the gate, not
audit_safely: knowing what an operation is about to touch is part of being able to record it, so
under critical: true a snapshot that cannot be read refuses the operation.
51 52 53 54 55 56 57 58 |
# File 'lib/forest_admin_agent/audit_trail/snapshots.rb', line 51 def take(context, projection) cap = AuditTrail::MAX_RECORDS_PER_OPERATION page = ForestAdminDatasourceToolkit::Components::Query::Page.new(offset: 0, limit: cap + 1) records = AuditTrail.gate { context.collection.list(context.filter.override(page: page), projection) } || [] return records if records.size <= cap refuse_or_truncate(context, records, cap) end |