Class: StandardLedger::Modes::Inline
- Inherits:
-
Object
- Object
- StandardLedger::Modes::Inline
- Defined in:
- lib/standard_ledger/modes/inline.rb
Overview
‘:inline` mode: applies the projection inside the entry’s ‘after_create` callback, which fires while the host’s outer transaction is still open. If the host’s transaction rolls back, the projection rolls back too.
This is the default for delta-based counter updates. For complex projectors (jsonb shape, multi-row aggregates), use ‘:async` instead.
The strategy is invoked from a single ‘after_create` callback installed once per entry class (see `.install!`). The callback walks every `:inline`-mode projection registered on the class and runs each via `entry.apply_projection!(definition)`.
## Multi-counter coalescing
A single host might register four ‘on(:grant)`/`on(:redeem)`/… handlers against the same target, each calling `target.increment(:some_count)`. ActiveRecord’s ‘#increment` (vs. `#increment!`) only mutates the in-memory attribute — no SQL is issued — so the strategy persists with a single `target.save!` per (entry, target) pair after all handlers for that target have run. This collapses N handlers into one UPDATE.
Definitions targeting different associations get their own apply-then-save cycle, executed in the order the projections were declared.
## Lock semantics
When any projection in a per-target group declares ‘lock: :pessimistic`, the strategy wraps the entire apply-then-save cycle for that target in `target.with_lock { … }`. The lock spans both handler invocation and the coalesced `save!`, so concurrent posts to the same target serialize end-to-end — closing the lost-update window that an inner-only lock would leave open between the lock release and the save. See `standard_ledger-design.md` §5.3.1.
Class Method Summary collapse
-
.install!(entry_class) ⇒ void
Install the ‘after_create` callback on `entry_class` exactly once.
Instance Method Summary collapse
-
#call(entry) ⇒ void
Apply every ‘:inline` projection registered on the entry’s class.
Class Method Details
.install!(entry_class) ⇒ void
This method returns an undefined value.
Install the ‘after_create` callback on `entry_class` exactly once. Subsequent calls (e.g. when a second `:inline` projection is added later in the class body) are no-ops — the same callback handles all `:inline` projections registered on the class.
49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
# File 'lib/standard_ledger/modes/inline.rb', line 49 def self.install!(entry_class) return if entry_class.instance_variable_get(:@_standard_ledger_inline_installed) unless entry_class.respond_to?(:after_create) raise ArgumentError, "#{entry_class.name || entry_class.inspect} cannot use mode: :inline " \ "because it does not respond to `after_create`. `:inline` mode requires " \ "an ActiveRecord-backed entry class — use `:async` (or another mode) for " \ "non-AR includers." end entry_class.after_create { StandardLedger::Modes::Inline.new.call(self) } entry_class.instance_variable_set(:@_standard_ledger_inline_installed, true) end |
Instance Method Details
#call(entry) ⇒ void
This method returns an undefined value.
Apply every ‘:inline` projection registered on the entry’s class. Called from the ‘after_create` callback installed by `.install!`.
Projections targeting the same association coalesce: all handlers for that target run, then the target is saved once. Different targets get their own apply+save cycle, in declared order. When any definition in a per-target group sets ‘lock: :pessimistic`, the cycle (apply + save) is wrapped in `target.with_lock`.
Records the names of projections that actually ran (after ‘if:` guards filter) on the entry instance under `@_standard_ledger_applied_projections`, so `StandardLedger.post` can surface an accurate `result.projections`.
Any projector exception escapes — the entry’s transaction rolls back along with every counter mutation that ran before the failure. The ‘standard_ledger.projection.failed` notification fires before the re-raise so subscribers see the failed projection in payload.
85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 |
# File 'lib/standard_ledger/modes/inline.rb', line 85 def call(entry) definitions = inline_definitions_for(entry.class) return if definitions.empty? applied = [] entry.instance_variable_set(:@_standard_ledger_applied_projections, applied) # group_by preserves insertion order on Ruby >= 1.9, so declared # projection order is preserved across targets. Within a target # group, handlers run in declared order as well. definitions.group_by(&:target_association).each_value do |group| target = entry.public_send(group.first.target_association) locked = group.any? { |definition| definition.lock == :pessimistic } run_group = lambda do group.each do |definition| ran = instrument_projection(entry, target, definition) do entry.apply_projection!(definition) end applied << definition.target_association if ran && !applied.include?(definition.target_association) end # Coalesce: if any handler called `target.increment(col)` (which # mutates in-memory only), persist the accumulated changes with a # single UPDATE. Skipped when the target is nil (apply_projection! # short-circuits) or when no handler dirtied the record. The # `target` here always responds to AR's `changed?`/`save!` because # it's resolved from a `belongs_to` reflection. target.save! if target && target.changed? end if locked && target.respond_to?(:with_lock) target.with_lock(&run_group) else run_group.call end end end |