Class: ForestAdminAgent::AuditTrail::Snapshots

Inherits:
Object
  • Object
show all
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

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