Class: ViewModel::ActiveRecord::UpdateContext

Inherits:
Object
  • Object
show all
Includes:
ErrorWrapping
Defined in:
lib/view_model/active_record/update_context.rb

Defined Under Namespace

Classes: ReleaseEntry, ReleasePool

Class Method Summary collapse

Instance Method Summary collapse

Methods included from ErrorWrapping

#wrap_active_record_errors

Constructor Details

#initializeUpdateContext

Returns a new instance of UpdateContext.



79
80
81
82
83
84
85
86
87
88
89
90
91
92
# File 'lib/view_model/active_record/update_context.rb', line 79

def initialize
  @root_update_operations       = [] # The subject(s) of this update
  @referenced_update_operations = {} # data updates to other root models, referred to by a ref hash

  # Set of ViewModel::Reference used to assert only a single update is
  # present for each viewmodel
  @updated_viewmodel_references = Set.new

  # hash of { ViewModel::Reference => deferred UpdateOperation }
  # for linked partially-constructed node updates
  @worklist = {}

  @release_pool = ReleasePool.new
end

Class Method Details

.build!(root_update_data, referenced_update_data, root_type: nil) ⇒ Object



63
64
65
66
67
68
69
70
71
# File 'lib/view_model/active_record/update_context.rb', line 63

def self.build!(root_update_data, referenced_update_data, root_type: nil)
  if root_type.present? && (bad_types = root_update_data.map(&:viewmodel_class).to_set.delete(root_type)).present?
    raise ViewModel::DeserializationError::InvalidViewType.new(root_type.view_name, bad_types.map { |t| ViewModel::Reference.new(t, nil) })
  end

  self.new
    .build_root_update_operations(root_update_data, referenced_update_data)
    .assemble_update_tree
end

Instance Method Details

#assemble_update_treeObject



187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
# File 'lib/view_model/active_record/update_context.rb', line 187

def assemble_update_tree
  @root_update_operations.each do |root_update|
    root_update.build!(self)
  end

  while @worklist.present?
    key = @worklist.keys.detect { |k| @release_pool.include?(k) }
    if key.nil?
      raise ViewModel::DeserializationError::ParentNotFound.new(@worklist.keys)
    end

    deferred_update    = @worklist.delete(key)
    released_viewmodel = @release_pool.claim_from_pool(key)

    if deferred_update.viewmodel
      # Deferred reference updates already have a viewmodel: ensure it
      # matches the tree
      unless deferred_update.viewmodel == released_viewmodel
        raise ViewModel::DeserializationError::Internal.new(
                "Released viewmodel doesn't match reference update", blame_reference)
      end
    else
      deferred_update.viewmodel = released_viewmodel
    end

    deferred_update.build!(self)
  end

  dangling_references = @referenced_update_operations.reject { |_ref, upd| upd.built? }.map { |_ref, upd| upd.viewmodel.to_reference }
  if dangling_references.present?
    raise ViewModel::DeserializationError::InvalidStructure.new('References not referred to from roots', dangling_references)
  end

  self
end

#build_root_update_operations(root_updates, referenced_updates) ⇒ Object

Processes parsed (UpdateData) root updates and referenced updates into



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
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
# File 'lib/view_model/active_record/update_context.rb', line 96

def build_root_update_operations(root_updates, referenced_updates)
  # Look up viewmodel classes for each tree with eager_includes. Note this
  # won't yet include through a polymorphic boundary: for now we become
  # lazy-loading and slow every time that happens.

  # Combine our root and referenced updates, and separate by viewmodel type.
  # Sort by id where possible to obtain as close to possible a deterministic
  # update order to avoid database write deadlocks. This can't be entirely
  # comprehensive, since we can't control the order that shared references
  # are referred to from roots (and therefore visited).
  updates_by_viewmodel_class =
    root_updates.lazily
      .map { |root_update| [nil, root_update] }
      .concat(referenced_updates)
      .sort_by { |_, update_data| update_data..id.to_s }
      .group_by { |_, update_data| update_data.viewmodel_class }

  # For each viewmodel type, look up referenced models and construct viewmodels to update
  updates_by_viewmodel_class.each do |viewmodel_class, updates|
    dependencies = updates.map { |_, upd| upd.preload_dependencies }
                   .inject { |acc, deps| acc.merge!(deps) }

    model_ids = updates.map { |_, update_data| update_data.id unless update_data.new? }.compact

    existing_models =
      if model_ids.present?
        model_class = viewmodel_class.model_class
        models = model_class.where(model_class.primary_key => model_ids).to_a

        if models.size < model_ids.size
          missing_model_ids = model_ids - models.map(&:id)
          missing_viewmodel_refs = missing_model_ids.map { |id| ViewModel::Reference.new(viewmodel_class, id) }
          raise ViewModel::DeserializationError::NotFound.new(missing_viewmodel_refs)
        end

        DeepPreloader.preload(models, dependencies)
        models.index_by(&:id)
      else
        {}
      end

    updates.each do |ref, update_data|
      viewmodel =
        if update_data.auto_child_update?
          raise ViewModel::DeserializationError::InvalidStructure.new(
                  'Cannot make an automatic child update to a root node',
                  ViewModel::Reference.new(update_data.viewmodel_class, nil))
        elsif update_data.child_update?
          raise ViewModel::DeserializationError::InvalidStructure.new(
                  'Cannot update an existing root node without a specified id',
                  ViewModel::Reference.new(update_data.viewmodel_class, nil))
        elsif update_data.new?
          viewmodel_class.for_new_model(id: update_data.id)
        else
          viewmodel_class.new(existing_models[update_data.id])
        end

      update_op = new_update(viewmodel, update_data)

      if ref.nil?
        @root_update_operations << update_op
      else
        # TODO: make sure that referenced subtree hashes are unique and provide a decent error message
        # not strictly necessary, but will save confusion
        @referenced_update_operations[ref] = update_op
      end
    end
  end

  self
end

#check_deferred_constraints!(model_class) ⇒ Object

Immediately enforce any deferred database constraints (when using Postgres) and convert them to DeserializationErrors.

Note that there’s no effective way to tie such a failure back to the individual node that caused it, without attempting to parse Postgres’ human-readable error details.



279
280
281
282
283
284
285
# File 'lib/view_model/active_record/update_context.rb', line 279

def check_deferred_constraints!(model_class)
  if model_class.connection.adapter_name == 'PostgreSQL'
    wrap_active_record_errors([]) do
      model_class.connection.execute('SET CONSTRAINTS ALL IMMEDIATE')
    end
  end
end

#check_unique_update!(vm_ref) ⇒ Object



253
254
255
256
257
# File 'lib/view_model/active_record/update_context.rb', line 253

def check_unique_update!(vm_ref)
  unless @updated_viewmodel_references.add?(vm_ref)
    raise ViewModel::DeserializationError::DuplicateNodes.new(vm_ref.viewmodel_class.view_name, vm_ref)
  end
end

#defer_update(viewmodel_reference, update_operation) ⇒ Object

Defer an existing update: used if we need to ensure that an owned reference has been freed before we use it.



238
239
240
# File 'lib/view_model/active_record/update_context.rb', line 238

def defer_update(viewmodel_reference, update_operation)
  @worklist[viewmodel_reference] = update_operation
end

#new_deferred_update(viewmodel_reference, update_data, reparent_to: nil, reposition_to: nil) ⇒ Object

We require the updates to be recorded in the context so we can enforce the property that each viewmodel is in the tree at most once. To avoid mistakes, we require construction to go via methods that do this tracking.



229
230
231
232
233
234
# File 'lib/view_model/active_record/update_context.rb', line 229

def new_deferred_update(viewmodel_reference, update_data, reparent_to: nil, reposition_to: nil)
  update_operation = ViewModel::ActiveRecord::UpdateOperation.new(
    nil, update_data, reparent_to: reparent_to, reposition_to: reposition_to)
  check_unique_update!(viewmodel_reference)
  defer_update(viewmodel_reference, update_operation)
end

#new_update(viewmodel, update_data, reparent_to: nil, reposition_to: nil) ⇒ Object



242
243
244
245
246
247
248
249
250
251
# File 'lib/view_model/active_record/update_context.rb', line 242

def new_update(viewmodel, update_data, reparent_to: nil, reposition_to: nil)
  update = ViewModel::ActiveRecord::UpdateOperation.new(
    viewmodel, update_data, reparent_to: reparent_to, reposition_to: reposition_to)

  if (vm_ref = update.viewmodel_reference).present?
    check_unique_update!(vm_ref)
  end

  update
end

#release_viewmodel(viewmodel, association_data) ⇒ Object



269
270
271
# File 'lib/view_model/active_record/update_context.rb', line 269

def release_viewmodel(viewmodel, association_data)
  @release_pool.release_to_pool(viewmodel, association_data)
end

#resolve_reference(ref, blame_reference) ⇒ Object



259
260
261
262
263
# File 'lib/view_model/active_record/update_context.rb', line 259

def resolve_reference(ref, blame_reference)
  @referenced_update_operations.fetch(ref) do
    raise ViewModel::DeserializationError::InvalidSharedReference.new(ref, blame_reference)
  end
end

#root_updatesObject

TODO: an unfortunate abstraction violation. The append case constructs an update tree and later injects the context of parent and position.



75
76
77
# File 'lib/view_model/active_record/update_context.rb', line 75

def root_updates
  @root_update_operations
end

#run!(deserialize_context:) ⇒ Object

Applies updates and subsequently releases. Returns the updated viewmodels.



169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
# File 'lib/view_model/active_record/update_context.rb', line 169

def run!(deserialize_context:)
  updated_viewmodels = @root_update_operations.map do |root_update|
    root_update.run!(deserialize_context: deserialize_context)
  end

  @release_pool.release_all!

  if updated_viewmodels.present? && deserialize_context.validate_deferred_constraints?
    # Deferred database constraints may have been violated by changes during
    # deserialization. VM::AR promises that any errors during deserialization
    # will be raised as a ViewModel::DeserializationError, so check constraints
    # and raise before exit.
    check_deferred_constraints!(updated_viewmodels.first.model.class)
  end

  updated_viewmodels
end

#try_take_released_viewmodel(vm_ref) ⇒ Object



265
266
267
# File 'lib/view_model/active_record/update_context.rb', line 265

def try_take_released_viewmodel(vm_ref)
  @release_pool.claim_from_pool(vm_ref)
end