Class: InstantRecord::MutationApplier
- Inherits:
-
Object
- Object
- InstantRecord::MutationApplier
- Defined in:
- app/models/instant_record/mutation_applier.rb
Overview
Applies one client mutation authoritatively: record write, version bump, change-log row, and idempotency-ledger row, in one transaction. This is the server half of the sync protocol; the HTTP layer above it is thin.
Class Method Summary collapse
Instance Method Summary collapse
-
#apply ⇒ Object
Replayed mutation ids return their original result without re-applying.
-
#initialize(mutation) ⇒ MutationApplier
constructor
A new instance of MutationApplier.
Constructor Details
#initialize(mutation) ⇒ MutationApplier
Returns a new instance of MutationApplier.
10 11 12 13 |
# File 'app/models/instant_record/mutation_applier.rb', line 10 def initialize(mutation) @mutation = mutation @changes = (mutation[:changes] || {}).to_h end |
Class Method Details
.apply(mutation) ⇒ Object
6 7 8 |
# File 'app/models/instant_record/mutation_applier.rb', line 6 def self.apply(mutation) new(mutation).apply end |
Instance Method Details
#apply ⇒ Object
Replayed mutation ids return their original result without re-applying.
16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
# File 'app/models/instant_record/mutation_applier.rb', line 16 def apply if (existing = AppliedMutation.find_by(mutation_id: @mutation[:id])) return existing.result_payload.symbolize_keys end model = InstantRecord.synced_model(@mutation[:record_type]) return record_result(status: "rejected", reason: "unknown record type") unless model result = ActiveRecord::Base.transaction do InstantRecord. { perform(model) } end record_result(**result) rescue ActiveRecord::RecordInvalid => e record_result(status: "rejected", reason: e.record.errors..to_sentence, server_attributes: server_attributes_for(model)) rescue ActiveRecord::RecordNotUnique # A create whose id already exists (e.g. a client replaying rows the # server already has). Rejecting lets the client roll back its local # copy instead of retrying the poisoned mutation forever; the server's # row comes back down through the change stream. record_result(status: "rejected", reason: "id already exists", server_attributes: server_attributes_for(model)) rescue ActiveModel::UnknownAttributeError => e # Schema skew: this client is a migration ahead of this server and sent a # column that does not exist here. Rejecting is the only outcome that # cannot jam the queue. Raising 500s the entire batch, and because the # client keeps the row on a transport error it would retry the same # poisoned mutation forever, blocking every write queued behind it. # Slicing the unknown column off instead would answer "applied" for a # write the server only partly stored — silent data loss the client has no # way to notice. A surfaced rejection loses the same write loudly, rolls # the local record back to what the server actually holds, and leaves the # queue draining. Deploy order (server first) remains the real defence. record_result(status: "rejected", reason: e., server_attributes: server_attributes_for(model)) end |