Class: Petra::Components::Section

Inherits:
Object
  • Object
show all
Defined in:
lib/petra/components/section.rb

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(transaction, savepoint: nil) ⇒ Section

Returns a new instance of Section.



10
11
12
13
14
# File 'lib/petra/components/section.rb', line 10

def initialize(transaction, savepoint: nil)
  @transaction = transaction
  @savepoint   = savepoint || next_savepoint_name
  load_persisted_log_entries
end

Instance Attribute Details

#savepointObject (readonly)

Returns the value of attribute savepoint.



8
9
10
# File 'lib/petra/components/section.rb', line 8

def savepoint
  @savepoint
end

#transactionObject (readonly)

Returns the value of attribute transaction.



7
8
9
# File 'lib/petra/components/section.rb', line 7

def transaction
  @transaction
end

Instance Method Details

#apply_log_entries!Object

See Also:

  • EntrySet#apply


429
430
431
# File 'lib/petra/components/section.rb', line 429

def apply_log_entries!
  log_entries.apply!
end

#attribute_change_vetoesObject

Holds all attribute change vetoes for the current section. If an attribute key is in this hash, it means that all previous changes made to it should be voided.

If an attribute is changed again after a veto was added, it is removed from this hash.



67
68
69
# File 'lib/petra/components/section.rb', line 67

def attribute_change_vetoes
  @attribute_change_vetoes ||= {}
end

#created_objectsArray<Petra::Proxies::ObjectProxy>

It does not matter whether the section was persisted or not in this case, the only condition is that the object was “object_persisted” after its initialization

Returns:



368
369
370
371
372
# File 'lib/petra/components/section.rb', line 368

def created_objects
  cache_if_persisted(:created_objects) do
    log_entries.of_kind(:object_initialization).object_persisted.map(&:load_proxy).uniq
  end
end

#destroyed_objectsArray<Petra::Proxies::ObjectProxies>

Returns Objects which were destroyed during the current section.

Returns:

  • (Array<Petra::Proxies::ObjectProxies>)

    Objects which were destroyed during the current section



400
401
402
403
404
# File 'lib/petra/components/section.rb', line 400

def destroyed_objects
  cache_if_persisted(:destroyed_objects) do
    log_entries.of_kind(:object_destruction).map(&:load_proxy).uniq
  end
end

#enqueue_for_persisting!Object



436
437
438
439
# File 'lib/petra/components/section.rb', line 436

def enqueue_for_persisting!
  log_entries.enqueue_for_persisting!
  @persisted = true
end

#initialized_objectsArray<Petra::Proxies::ObjectProxy>

Returns Objects which were initialized, but not yet persisted during this section. This may only be the case for the current section.

Returns:

  • (Array<Petra::Proxies::ObjectProxy>)

    Objects which were initialized, but not yet persisted during this section. This may only be the case for the current section



378
379
380
381
382
# File 'lib/petra/components/section.rb', line 378

def initialized_objects
  cache_if_persisted(:initialized_objects) do
    log_entries.of_kind(:object_initialization).not_object_persisted.map(&:load_proxy).uniq
  end
end

#initialized_or_created_objectsObject

This method will also return objects which were not yet ‘object_persisted`, e.g. to be used during the current transaction section

See Also:



390
391
392
393
394
# File 'lib/petra/components/section.rb', line 390

def initialized_or_created_objects
  cache_if_persisted(:initialized_or_created_objects) do
    (initialized_objects + created_objects).uniq
  end
end

#log_attribute_change(proxy, attribute:, old_value:, new_value:, method: nil) ⇒ Object

Generates a log entry for an attribute change in a certain object. If old and new value are the same, no log entry is created.

Parameters:

  • proxy (Petra::Components::ObjectProxy)

    The proxy which received the method call to change the attribute

  • attribute (String, Symbol)

    The name of the attribute which was changed

  • old_value (Object)

    The attribute’s value before the change

  • new_value (Object)

    The attribute’s new value

  • method (String, Symbol) (defaults to: nil)

    The method which was used to change the attribute



153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
# File 'lib/petra/components/section.rb', line 153

def log_attribute_change(proxy, attribute:, old_value:, new_value:, method: nil)
  # Generate a read set entry if we didn't read this attribute before.
  # This is necessary as real attribute reads are not necessarily performed in the same section
  # as attribute changes and persistence (e.g. #edit and #update in Rails)
  # This has to be done even if the attribute wasn't really changed as the user most likely
  # saw the current value and therefore decided not to change it.
  unless transaction.read_attribute_value?(proxy, attribute: attribute)
    log_attribute_read(proxy, attribute: attribute, value: old_value, method: method)
  end

  return if old_value == new_value

  # Replace any existing value for the current attribute in the
  # memory write set with the new value
  add_to_write_set(proxy, attribute, new_value)
  add_log_entry(proxy,
                attribute: attribute,
                method:    method,
                kind:      'attribute_change',
                old_value: old_value,
                new_value: new_value)

  Petra.logger.info "Logged attribute change (#{old_value} => #{new_value})", :yellow
end

#log_attribute_change_veto(proxy, attribute:, external_value:) ⇒ Object

Logs the fact that the user decided to “undo” all previous changes made to the given attribute



293
294
295
296
297
298
299
300
301
# File 'lib/petra/components/section.rb', line 293

def log_attribute_change_veto(proxy, attribute:, external_value:)
  add_log_entry(proxy,
                kind:           'attribute_change_veto',
                attribute:      attribute,
                external_value: external_value)

  # Also log the current external attribute value, so the transaction uses the newest available one
  log_attribute_read(proxy, attribute: attribute, value: external_value, persist_on_retry: true)
end

#log_attribute_read(proxy, attribute:, value:, method: nil, **options) ⇒ Object

Generates a log entry for an attribute read in a certain object.



183
184
185
186
187
188
189
190
191
192
193
194
# File 'lib/petra/components/section.rb', line 183

def log_attribute_read(proxy, attribute:, value:, method: nil, **options)
  add_to_read_set(proxy, attribute, value)
  add_log_entry(proxy,
                attribute: attribute,
                method:    method,
                kind:      'attribute_read',
                value:     value,
                **options)

  Petra.logger.info "Logged attribute read (#{attribute} => #{value})", :yellow
  true
end

#log_entriesPetra::Components::EntrySet



130
131
132
# File 'lib/petra/components/section.rb', line 130

def log_entries
  @log_entries ||= EntrySet.new
end

#log_object_destruction(proxy, method: nil) ⇒ Object

Logs the destruction of an object. Currently, this is only used with ActiveRecord::Base instances, but there might be a way to handle GC with normal ruby objects (attach a handler to at least get notified).



247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
# File 'lib/petra/components/section.rb', line 247

def log_object_destruction(proxy, method: nil)
  # Destruction is a form of persistence, resp. its opposite.
  # Therefore, we have to make sure that any other log entries for this
  # object will be transaction persisted as the may have lead to the object's destruction.
  #
  # Currently, this happens even if the object hasn't been persisted prior to
  # its destruction which is accepted behaviour e.g. by ActiveRecord instances.
  # We'll have to see if this should stay the common behaviour.
  log_entries.for_proxy(proxy).each(&:mark_as_object_persisted!)

  # As for attribute persistence, every attribute which was read in the current section
  # might have had impact on the destruction of this object. Therefore, we have
  # to make sure that all these log entries will be persisted.
  log_entries.of_kind(:attribute_read).each(&:mark_as_object_persisted!)

  add_log_entry(proxy,
                kind:             'object_destruction',
                method:           method,
                object_persisted: true)
  true
end

#log_object_initialization(proxy, method: nil) ⇒ Object

Logs the initialization of an object



199
200
201
202
203
204
205
206
207
# File 'lib/petra/components/section.rb', line 199

def log_object_initialization(proxy, method: nil)
  # Mark this object as recently initialized
  recently_initialized_object!(proxy)

  add_log_entry(proxy,
                kind:   'object_initialization',
                method: method)
  true
end

#log_object_persistence(proxy, method: nil, args: []) ⇒ Object

Logs the persistence of an object. This basically means that the attribute updates were written to a shared memory. This might simply be the process memory for normal ruby objects, but might also be a call to save() or update() for ActiveRecord::Base instances.

Parameters:

  • proxy (Petra::Components::ObjectProxy)

    The proxy which received the method call

  • method (String, Symbol) (defaults to: nil)

    The method which caused the persistence change



220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
# File 'lib/petra/components/section.rb', line 220

def log_object_persistence(proxy, method: nil, args: [])
  # All log entries for the current object prior to this persisting method
  # have to be persisted as the object itself is.
  # This includes the object initialization log entry
  log_entries.for_proxy(proxy).each(&:mark_as_object_persisted!)

  # All attribute reads prior to this have to be persisted
  # as they might have had impact on the current object state.
  # This does not only include the current object, but everything that was
  # read until now!
  # TODO: Could this be more intelligent?
  log_entries.of_kind(:attribute_read).each(&:mark_as_object_persisted!)

  add_log_entry(proxy,
                method:           method,
                kind:             'object_persistence',
                object_persisted: true,
                args:             args)

  true
end

#log_read_integrity_override(proxy, attribute:, external_value:, update_value: false) ⇒ Object

Logs the fact that the user decided to ignore further ReadIntegrityErrors on the given attribute as long as its external value stays the same.

Parameters:

  • update_value (Boolean) (defaults to: false)

    If true, a new read set entry is generated along with the RIO one. This will cause the transaction to display the new external value instead of the one we last read and will also automatically invalidate the RIO entry which is only kept to have the whole transaction time line.



279
280
281
282
283
284
285
286
287
# File 'lib/petra/components/section.rb', line 279

def log_read_integrity_override(proxy, attribute:, external_value:, update_value: false)
  add_log_entry(proxy,
                kind:           'read_integrity_override',
                attribute:      attribute,
                external_value: external_value)

  # If requested, add a new read log entry for the new external value
  log_attribute_read(proxy, attribute: attribute, value: external_value, persist_on_retry: true) if update_value
end

#objectsArray<Petra::Proxies::ObjectProxy>

Returns All Objects that were part of this section. Only log entries marked as object persisted are taken into account.

Returns:

  • (Array<Petra::Proxies::ObjectProxy>)

    All Objects that were part of this section. Only log entries marked as object persisted are taken into account



346
347
348
349
350
# File 'lib/petra/components/section.rb', line 346

def objects
  cache_if_persisted(:all_objects) do
    log_entries.object_persisted.map(&:load_proxy).uniq
  end
end

#persisted?Boolean

Returns:

  • (Boolean)


23
24
25
# File 'lib/petra/components/section.rb', line 23

def persisted?
  !!@persisted
end

#prepare_for_retry!Object



422
423
424
# File 'lib/petra/components/section.rb', line 422

def prepare_for_retry!
  log_entries.prepare_for_retry!
end

#read_attributesHash<Petra::Proxies::ObjectProxy, Array<String,Symbol>>

Only entries which were previously marked as object persisted are taken into account.

Returns:

  • (Hash<Petra::Proxies::ObjectProxy, Array<String,Symbol>>)

    All attributes which were read during this section grouped by the objects (proxies) they belong to.



333
334
335
336
337
338
339
340
# File 'lib/petra/components/section.rb', line 333

def read_attributes
  cache_if_persisted(:read_attributes) do
    log_entries.of_kind(:attribute_read).object_persisted.each_with_object({}) do |entry, h|
      h[entry.load_proxy] ||= []
      h[entry.load_proxy] << entry.attribute unless h[entry.load_proxy].include?(entry.attribute)
    end
  end
end

#read_integrity_override(proxy, attribute:) ⇒ Object

Returns The external value at the time the requested read integrity override was placed.

Returns:

  • (Object)

    The external value at the time the requested read integrity override was placed.



119
120
121
# File 'lib/petra/components/section.rb', line 119

def read_integrity_override(proxy, attribute:)
  read_integrity_overrides[proxy.__attribute_key(attribute)]
end

#read_integrity_override?(proxy, attribute:) ⇒ Boolean

Returns true if there is a read integrity override for the given attribute name.

Returns:

  • (Boolean)

    true if there is a read integrity override for the given attribute name



111
112
113
# File 'lib/petra/components/section.rb', line 111

def read_integrity_override?(proxy, attribute:)
  read_integrity_overrides.key?(proxy.__attribute_key(attribute))
end

#read_integrity_overridesObject

Holds all read integrity overrides which were generated during this section. There should normally only be one per section.

The hash maps attribute keys to the external value at the time the corresponding log entry was generated. Please take a look at Petra::Components::Entries::ReadIntegrityOverride for more information about this kind of log entry.



55
56
57
# File 'lib/petra/components/section.rb', line 55

def read_integrity_overrides
  @read_integrity_overrides ||= {}
end

#read_objectsArray<Petra::Proxies::ObjectProxy>

Returns Objects that were read during this section Only read log entries which were marked as object persisted are taken into account.

Returns:

  • (Array<Petra::Proxies::ObjectProxy>)

    Objects that were read during this section Only read log entries which were marked as object persisted are taken into account



356
357
358
359
360
# File 'lib/petra/components/section.rb', line 356

def read_objects
  cache_if_persisted(:read_objects) do
    read_attributes.keys
  end
end

#read_setObject

Holds the values which were last read from attribute readers



34
35
36
# File 'lib/petra/components/section.rb', line 34

def read_set
  @read_set ||= {}
end

#read_value_for(proxy, attribute:) ⇒ Object, NilClass

Returns the attribute value which was read from the original object during this section or nil. Please check whether the attribute was read at all during this section using #read_value_for?.

Returns:

  • (Object, NilClass)

    the attribute value which was read from the original object during this section or nil. Please check whether the attribute was read at all during this section using #read_value_for?



103
104
105
# File 'lib/petra/components/section.rb', line 103

def read_value_for(proxy, attribute:)
  read_set[proxy.__attribute_key(attribute)]
end

#read_value_for?(proxy, attribute:) ⇒ Boolean

Returns true if a new object attribute with the given name was read during this section. Each attribute is only put into the read set once - except for when the read value wasn’t used afterwards (no persistence).

Returns:

  • (Boolean)

    true if a new object attribute with the given name was read during this section. Each attribute is only put into the read set once - except for when the read value wasn’t used afterwards (no persistence)



94
95
96
# File 'lib/petra/components/section.rb', line 94

def read_value_for?(proxy, attribute:)
  read_set.key?(proxy.__attribute_key(attribute))
end

#recently_initialized_object!(proxy) ⇒ Object



318
319
320
# File 'lib/petra/components/section.rb', line 318

def recently_initialized_object!(proxy)
  recently_initialized_objects << proxy.send(:proxied_object).object_id
end

#recently_initialized_object?(proxy) ⇒ Boolean

Returns:

  • (Boolean)


322
323
324
# File 'lib/petra/components/section.rb', line 322

def recently_initialized_object?(proxy)
  recently_initialized_objects.include?(proxy.send(:proxied_object).object_id)
end

#recently_initialized_objectsObject

As objects which were initialized inside a transaction receive a temporary ID whose generation again requires knowledge about their membership regarding the below object sets leading to an infinite loop, we have to keep a temporary list of object ids (ruby) until they received their transaction object id



314
315
316
# File 'lib/petra/components/section.rb', line 314

def recently_initialized_objects
  @recently_initialized_objects ||= []
end

#reset!Object

Removes all log entries and empties the read and write set. This should only be done on the current section and as long as the log entries haven’t been persisted.



415
416
417
418
419
420
# File 'lib/petra/components/section.rb', line 415

def reset!
  fail Petra::PetraError, 'An already persisted section may not be reset' if persisted?
  @log_entries = []
  @read_set    = []
  @write_set   = []
end

#savepoint_versionFixnum

Returns the savepoint’s version number.

Returns:

  • (Fixnum)

    the savepoint’s version number



19
20
21
# File 'lib/petra/components/section.rb', line 19

def savepoint_version
  savepoint.split('/')[1].to_i
end

#value_for(proxy, attribute:) ⇒ Object, NilClass

Returns the value which was set for the given attribute during this session. Please note that setting attributes to nil is normal behaviour, so please make sure you always check whether there actually is value in the write set using #value_for?.

Returns:

  • (Object, NilClass)

    the value which was set for the given attribute during this session. Please note that setting attributes to nil is normal behaviour, so please make sure you always check whether there actually is value in the write set using #value_for?



77
78
79
# File 'lib/petra/components/section.rb', line 77

def value_for(proxy, attribute:)
  write_set[proxy.__attribute_key(attribute)]
end

#value_for?(proxy, attribute:) ⇒ Boolean

Returns true if this section’s write set contains a value for the given attribute (if a new value was set during this section).

Returns:

  • (Boolean)

    true if this section’s write set contains a value for the given attribute (if a new value was set during this section)



85
86
87
# File 'lib/petra/components/section.rb', line 85

def value_for?(proxy, attribute:)
  write_set.key?(proxy.__attribute_key(attribute))
end

#write_setObject

The write set in a section only holds the latest value for each attribute/object combination. The change history is done using log entries. Therefore, the write set is a simple hash mapping object-attribute-keys to their latest value.



43
44
45
# File 'lib/petra/components/section.rb', line 43

def write_set
  @write_set ||= {}
end