Class: Reeve::Audit::IsolatedRecorder

Inherits:
Recorder
  • Object
show all
Defined in:
lib/reeve/audit/isolated_recorder.rb

Overview

The ledger row, written on a connection of its own.

Recorder writes in a savepoint, which protects the trace from a transaction the tool opens and rolls back. It cannot protect the trace from a transaction the host opened around the whole invocation — a controller that wraps each request in one, a job runner, a test suite with transactional fixtures. A savepoint released into that transaction goes down with it, and the envelope has no way to learn otherwise: the write reported success, the invocation returned records, and no row survives to say so. Constitution II says every call leaves a trace; on one connection that is not achievable, only detectable, which is why Recorder warns.

A second connection is the only thing that actually closes it. A transaction on connection A cannot roll back an INSERT committed on connection B, so the row lands whatever the host's transaction does next.

config.audit_recorder = Reeve::Audit::IsolatedRecorder

Why this is not the default

It cannot be. On SQLite the host's open transaction holds the database write lock, so the second connection blocks until it times out and every guarded call fails — the isolation is not merely unavailable there, it is actively worse than the savepoint. Rather than degrade quietly on the database most hosts develop against, this refuses to run on SQLite and says why. Recorder remains the default, and a host that needs durability under a wrapping transaction opts in on a database where opting in means something.

It also costs a connection per pool, and moves the ledger write outside the host's transaction in both directions: the row survives a rollback, which is the point, and it also survives a rollback of an invocation the host meant to undo entirely. A ledger of what was attempted is the intended reading of Constitution II, but it is a choice, and it should be made rather than inherited.

Constant Summary collapse

UNSUPPORTED_ADAPTERS =
%w[sqlite sqlite3].freeze

Constants inherited from Recorder

Recorder::DETAIL_LIMIT

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(config: nil) ⇒ IsolatedRecorder

Returns a new instance of IsolatedRecorder.



69
70
71
# File 'lib/reeve/audit/isolated_recorder.rb', line 69

def initialize(config: nil)
  super(entry_class: IsolatedEntry, config: config)
end

Class Method Details

.available?Boolean

True when a second connection can actually be used here. Lets a host branch in an initializer — SQLite in development, PostgreSQL in production — rather than discovering the answer on the first guarded call.

Returns:

  • (Boolean)


48
49
50
51
52
53
# File 'lib/reeve/audit/isolated_recorder.rb', line 48

def available?
  verify_supported!
  true
rescue Reeve::Error
  false
end

.record(attributes) ⇒ Object



41
42
43
# File 'lib/reeve/audit/isolated_recorder.rb', line 41

def record(attributes)
  new.record(attributes)
end

.verify_supported!Object

Raises:



55
56
57
58
59
60
61
62
63
64
65
66
# File 'lib/reeve/audit/isolated_recorder.rb', line 55

def verify_supported!
  adapter = IsolatedEntry.adapter_name
  return unless UNSUPPORTED_ADAPTERS.include?(adapter.to_s.downcase)

  raise ConfigurationError,
        "Reeve::Audit::IsolatedRecorder cannot be used on #{adapter}: an open " \
        "transaction holds the database write lock, so a second connection " \
        "blocks until it times out and every guarded call fails. Use " \
        "Reeve::Audit::Recorder here — it warns when a host transaction is " \
        "wrapped around an invocation — and configure this recorder only where " \
        "the database supports concurrent writers."
end

Instance Method Details

#record(attributes) ⇒ Object

Checked out and returned around each write rather than held: the pool exists to keep the ledger off the host's connection, not to keep a connection per thread alive for the life of the process.



76
77
78
79
# File 'lib/reeve/audit/isolated_recorder.rb', line 76

def record(attributes)
  self.class.verify_supported!
  IsolatedEntry.connection_pool.with_connection { super }
end