Class: Clickwrap::ChainHead
- Inherits:
-
ApplicationRecord
- Object
- ActiveRecord::Base
- ApplicationRecord
- Clickwrap::ChainHead
- Defined in:
- lib/clickwrap/models/chain_head.rb
Overview
The head of one tamper-evident event chain.
Chaining is off unless configured. The scope is tenant-and-policy: a tenanted installation gets one chain per (tenant, policy). An UNTENANTED installation's scope is the literal "global/<policy_key>" — one chain per policy across all actors, which means every capture of that policy serializes behind every other. Enable chaining on hot policies knowing that queue exists; per-actor scoping is future work, not current behavior.
What a chain detects: an event rewritten or removed after the fact, as long as the head remains trustworthy. What it does not do: stop a party with full control of the application and database from rewriting both the events and the head. That is what the optional independent anchor adapter is for, and even then the claim is only as strong as the anchor.
Class Method Summary collapse
-
.record!(chain_scope:, event_id:, event_digest:) ⇒ Object
Records the digest the event was actually written with, so the next event in this scope links to something real.
-
.reserve!(chain_scope:) ⇒ Object
Takes the next position in the chain.
Instance Method Summary collapse
Class Method Details
.record!(chain_scope:, event_id:, event_digest:) ⇒ Object
Records the digest the event was actually written with, so the next event in this scope links to something real.
69 70 71 72 73 74 75 |
# File 'lib/clickwrap/models/chain_head.rb', line 69 def self.record!(chain_scope:, event_id:, event_digest:) head = lock.find_by(chain_scope: chain_scope) return nil unless head head.update!(last_event_id: event_id, last_event_digest: event_digest) head end |
.reserve!(chain_scope:) ⇒ Object
Takes the next position in the chain. The row lock is what stops two concurrent captures in the same scope from reading the same predecessor and forking the chain.
37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
# File 'lib/clickwrap/models/chain_head.rb', line 37 def self.reserve!(chain_scope:) # The first probe must be an ordinary read. On MySQL/InnoDB, two # `SELECT ... FOR UPDATE` calls for the same absent unique key both take # gap locks; when both then INSERT, InnoDB has to deadlock one of them. # Let the unique INSERT choose the first writer, absorb the loser's # duplicate in a savepoint, and only then take the row lock. head = find_by(chain_scope: chain_scope) unless head begin transaction(requires_new: true) { create!(chain_scope: chain_scope, chain_sequence: 0) } rescue ActiveRecord::RecordNotUnique # The savepoint absorbs PostgreSQL's aborted-statement state before # the winning row is read and locked in the caller's transaction. end end # Always re-read under the lock. A plain read above is only an existence # probe and may carry a stale sequence; this is the value from which the # next link is actually reserved. head = lock.find_by!(chain_scope: chain_scope) next_sequence = head.chain_sequence + 1 previous_digest = head.last_event_digest head.update!(chain_sequence: next_sequence) [previous_digest, next_sequence] end |
Instance Method Details
#to_s ⇒ Object
77 |
# File 'lib/clickwrap/models/chain_head.rb', line 77 def to_s = "chain #{chain_scope} at #{chain_sequence}" |