Class: OpenReceiveMeta

Inherits:
ActiveRecord::Base
  • Object
show all
Defined in:
app/models/open_receive_meta.rb

Overview

Engine-owned key/value/rev rows in the host database (the install generator emits the openreceive_meta table next to openreceive_payments). It holds the durable reconcile gate every Puma worker and process on this database shares, so rapid OpenReceive calls collapse to one real wallet scan per interval, and the installed schema-version marker the engine refuses to run past. Mirrors the JS SQL repository's claimReconcileGate and assertSupportedSchema.

Constant Summary collapse

RECONCILE_GATE_KEY =
"transaction_scan_gate"
SCHEMA_VERSION_KEY =
"schema_version"
CAS_RETRIES =
6
META_CLOCK_SKEW_SECONDS =

Tolerance when reading a timestamp another worker wrote. Beyond it a claim stamped in the future is a backwards clock step, not a fresh claim: without this clamp the gate would read as busy until wall-clock time caught up.

60

Class Method Summary collapse

Class Method Details

.assert_supported_schema!Object

One probe per process, on the engine's first database touch: a database written by a NEWER library must not be operated by this one (columns or state transitions it does not know about). An unreadable or absent marker means "not versioned" — the pre-versioned migrations could not seed a row — and is not a refusal. Mirrors the JS repository's assertSupportedSchema.



28
29
30
31
32
33
34
35
36
37
38
39
# File 'app/models/open_receive_meta.rb', line 28

def self.assert_supported_schema!
  @schema_version_checked ||= begin
    stored = stored_schema_version
    if !stored.nil? && stored > OpenReceive::Server::PAYMENTS_SCHEMA_VERSION
      raise OpenReceive::ConfigurationError,
            "openreceive_meta reports openreceive schema version #{stored}, newer than this " \
            "library's #{OpenReceive::Server::PAYMENTS_SCHEMA_VERSION}. Upgrade openreceive-rails " \
            "before serving this database."
    end
    true
  end
end

.cas(key, value, expected_rev) ⇒ Object

Optimistic compare-and-set: INSERT-if-absent at rev 0, or UPDATE ... WHERE rev = expected. Returns true when this caller's write won.



43
44
45
46
47
48
49
50
51
52
53
54
# File 'app/models/open_receive_meta.rb', line 43

def self.cas(key, value, expected_rev)
  if expected_rev.nil?
    begin
      create!(key: key, value: value, rev: 0)
      true
    rescue ActiveRecord::RecordNotUnique
      false
    end
  else
    where(key: key, rev: expected_rev).update_all(value: value, rev: expected_rev + 1) == 1
  end
end

.claim_reconcile_gate(now:, interval_seconds:) ⇒ Object

Claim the durable global reconcile gate. Returns true when this caller may run a wallet scan now; false (gate_busy) when another worker scanned within interval_seconds. The winner is identified by reading back its own token — the portable equivalent of an affected-row count, matching the JS claimReconcileGate. A failed scan leaves claimed_at in place on purpose — the next interval retries without a stampede.



62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
# File 'app/models/open_receive_meta.rb', line 62

def self.claim_reconcile_gate(now:, interval_seconds:)
  assert_supported_schema!
  claim = JSON.generate("claimed_at" => Integer(now), "token" => SecureRandom.uuid)
  CAS_RETRIES.times do
    row = find_by(key: RECONCILE_GATE_KEY)
    if row.nil?
      cas(RECONCILE_GATE_KEY, claim, nil)
    else
      claimed_at = parse_claimed_at(row.value)
      if claimed_at && fresh_timestamp?(Integer(now), claimed_at, Integer(interval_seconds))
        return false
      end
      cas(RECONCILE_GATE_KEY, claim, row.rev)
    end
    return true if where(key: RECONCILE_GATE_KEY).pick(:value) == claim
  end
  false
end