petra_core 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +9 -0
  3. data/.rspec +3 -0
  4. data/.rubocop.yml +83 -0
  5. data/.ruby-version +1 -0
  6. data/.travis.yml +5 -0
  7. data/CODE_OF_CONDUCT.md +74 -0
  8. data/Gemfile +13 -0
  9. data/Gemfile.lock +74 -0
  10. data/MIT-LICENSE +20 -0
  11. data/README.md +726 -0
  12. data/Rakefile +8 -0
  13. data/bin/console +8 -0
  14. data/bin/setup +8 -0
  15. data/examples/continuation_error.rb +125 -0
  16. data/examples/dining_philosophers.rb +138 -0
  17. data/examples/showcase.rb +54 -0
  18. data/lib/petra/components/entries/attribute_change.rb +29 -0
  19. data/lib/petra/components/entries/attribute_change_veto.rb +37 -0
  20. data/lib/petra/components/entries/attribute_read.rb +20 -0
  21. data/lib/petra/components/entries/object_destruction.rb +22 -0
  22. data/lib/petra/components/entries/object_initialization.rb +19 -0
  23. data/lib/petra/components/entries/object_persistence.rb +26 -0
  24. data/lib/petra/components/entries/read_integrity_override.rb +42 -0
  25. data/lib/petra/components/entry_set.rb +87 -0
  26. data/lib/petra/components/log_entry.rb +342 -0
  27. data/lib/petra/components/proxy_cache.rb +209 -0
  28. data/lib/petra/components/section.rb +543 -0
  29. data/lib/petra/components/transaction.rb +405 -0
  30. data/lib/petra/components/transaction_manager.rb +214 -0
  31. data/lib/petra/configuration/base.rb +132 -0
  32. data/lib/petra/configuration/class_configurator.rb +309 -0
  33. data/lib/petra/configuration/configurator.rb +67 -0
  34. data/lib/petra/core_ext.rb +27 -0
  35. data/lib/petra/exceptions.rb +181 -0
  36. data/lib/petra/persistence_adapters/adapter.rb +154 -0
  37. data/lib/petra/persistence_adapters/file_adapter.rb +239 -0
  38. data/lib/petra/proxies/abstract_proxy.rb +149 -0
  39. data/lib/petra/proxies/enumerable_proxy.rb +44 -0
  40. data/lib/petra/proxies/handlers/attribute_read_handler.rb +45 -0
  41. data/lib/petra/proxies/handlers/missing_method_handler.rb +47 -0
  42. data/lib/petra/proxies/method_handlers.rb +213 -0
  43. data/lib/petra/proxies/module_proxy.rb +12 -0
  44. data/lib/petra/proxies/object_proxy.rb +310 -0
  45. data/lib/petra/util/debug.rb +45 -0
  46. data/lib/petra/util/extended_attribute_accessors.rb +51 -0
  47. data/lib/petra/util/field_accessors.rb +35 -0
  48. data/lib/petra/util/registrable.rb +48 -0
  49. data/lib/petra/util/test_helpers.rb +9 -0
  50. data/lib/petra/version.rb +5 -0
  51. data/lib/petra.rb +100 -0
  52. data/lib/tasks/petra_tasks.rake +5 -0
  53. data/petra.gemspec +36 -0
  54. metadata +208 -0
@@ -0,0 +1,543 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Petra
4
+ module Components
5
+ class Section
6
+
7
+ attr_reader :transaction
8
+ attr_reader :savepoint
9
+
10
+ def initialize(transaction, savepoint: nil)
11
+ @transaction = transaction
12
+ @savepoint = savepoint || next_savepoint_name
13
+ load_persisted_log_entries
14
+ end
15
+
16
+ #
17
+ # @return [Fixnum] the savepoint's version number
18
+ #
19
+ def savepoint_version
20
+ savepoint.split('/')[1].to_i
21
+ end
22
+
23
+ def persisted?
24
+ !!@persisted
25
+ end
26
+
27
+ #----------------------------------------------------------------
28
+ # Read / Write set
29
+ #----------------------------------------------------------------
30
+
31
+ #
32
+ # Holds the values which were last read from attribute readers
33
+ #
34
+ def read_set
35
+ @read_set ||= {}
36
+ end
37
+
38
+ #
39
+ # The write set in a section only holds the latest value for each
40
+ # attribute/object combination. The change history is done using log entries.
41
+ # Therefore, the write set is a simple hash mapping object-attribute-keys to their latest value.
42
+ #
43
+ def write_set
44
+ @write_set ||= {}
45
+ end
46
+
47
+ #
48
+ # Holds all read integrity overrides which were generated during this section.
49
+ # There should normally only be one per section.
50
+ #
51
+ # The hash maps attribute keys to the external value at the time the corresponding
52
+ # log entry was generated. Please take a look at Petra::Components::Entries::ReadIntegrityOverride
53
+ # for more information about this kind of log entry.
54
+ #
55
+ def read_integrity_overrides
56
+ @read_integrity_overrides ||= {}
57
+ end
58
+
59
+ #
60
+ # Holds all attribute change vetoes for the current section.
61
+ # If an attribute key is in this hash, it means that all previous changes
62
+ # made to it should be voided.
63
+ #
64
+ # If an attribute is changed again after a veto was added, it is removed from
65
+ # this hash.
66
+ #
67
+ def attribute_change_vetoes
68
+ @attribute_change_vetoes ||= {}
69
+ end
70
+
71
+ #
72
+ # @return [Object, NilClass] the value which was set for the given attribute
73
+ # during this session. Please note that setting attributes to +nil+ is normal behaviour,
74
+ # so please make sure you always check whether there actually is value in the write set
75
+ # using #value_for?
76
+ #
77
+ def value_for(proxy, attribute:)
78
+ write_set[proxy.__attribute_key(attribute)]
79
+ end
80
+
81
+ #
82
+ # @return [Boolean] +true+ if this section's write set contains a value
83
+ # for the given attribute (if a new value was set during this section)
84
+ #
85
+ def value_for?(proxy, attribute:)
86
+ write_set.key?(proxy.__attribute_key(attribute))
87
+ end
88
+
89
+ #
90
+ # @return [Boolean] +true+ if a new object attribute with the given name
91
+ # was read during this section. Each attribute is only put into the read set once - except
92
+ # for when the read value wasn't used afterwards (no persistence)
93
+ #
94
+ def read_value_for?(proxy, attribute:)
95
+ read_set.key?(proxy.__attribute_key(attribute))
96
+ end
97
+
98
+ #
99
+ # @return [Object, NilClass] the attribute value which was read from the original object
100
+ # during this section or +nil+. Please check whether the attribute was read at all during
101
+ # this section using #read_value_for?
102
+ #
103
+ def read_value_for(proxy, attribute:)
104
+ read_set[proxy.__attribute_key(attribute)]
105
+ end
106
+
107
+ #
108
+ # @return [Boolean] +true+ if there is a read integrity override for
109
+ # the given attribute name
110
+ #
111
+ def read_integrity_override?(proxy, attribute:)
112
+ read_integrity_overrides.key?(proxy.__attribute_key(attribute))
113
+ end
114
+
115
+ #
116
+ # @return [Object] The external value at the time the requested
117
+ # read integrity override was placed.
118
+ #
119
+ def read_integrity_override(proxy, attribute:)
120
+ read_integrity_overrides[proxy.__attribute_key(attribute)]
121
+ end
122
+
123
+ #----------------------------------------------------------------
124
+ # Log Entries
125
+ #----------------------------------------------------------------
126
+
127
+ #
128
+ # @return [Petra::Components::EntrySet]
129
+ #
130
+ def log_entries
131
+ @log_entries ||= EntrySet.new
132
+ end
133
+
134
+ #
135
+ # Generates a log entry for an attribute change in a certain object.
136
+ # If old and new value are the same, no log entry is created.
137
+ #
138
+ # @param [Petra::Components::ObjectProxy] proxy
139
+ # The proxy which received the method call to change the attribute
140
+ #
141
+ # @param [String, Symbol] attribute
142
+ # The name of the attribute which was changed
143
+ #
144
+ # @param [Object] old_value
145
+ # The attribute's value before the change
146
+ #
147
+ # @param [Object] new_value
148
+ # The attribute's new value
149
+ #
150
+ # @param [String, Symbol] method
151
+ # The method which was used to change the attribute
152
+ #
153
+ def log_attribute_change(proxy, attribute:, old_value:, new_value:, method: nil)
154
+ # Generate a read set entry if we didn't read this attribute before.
155
+ # This is necessary as real attribute reads are not necessarily performed in the same section
156
+ # as attribute changes and persistence (e.g. #edit and #update in Rails)
157
+ # This has to be done even if the attribute wasn't really changed as the user most likely
158
+ # saw the current value and therefore decided not to change it.
159
+ unless transaction.read_attribute_value?(proxy, attribute: attribute)
160
+ log_attribute_read(proxy, attribute: attribute, value: old_value, method: method)
161
+ end
162
+
163
+ return if old_value == new_value
164
+
165
+ # Replace any existing value for the current attribute in the
166
+ # memory write set with the new value
167
+ add_to_write_set(proxy, attribute, new_value)
168
+ add_log_entry(proxy,
169
+ attribute: attribute,
170
+ method: method,
171
+ kind: 'attribute_change',
172
+ old_value: old_value,
173
+ new_value: new_value)
174
+
175
+ Petra.logger.info "Logged attribute change (#{old_value} => #{new_value})", :yellow
176
+ end
177
+
178
+ #
179
+ # Generates a log entry for an attribute read in a certain object.
180
+ #
181
+ # @see #log_attribute_change for parameter details
182
+ #
183
+ def log_attribute_read(proxy, attribute:, value:, method: nil, **options)
184
+ add_to_read_set(proxy, attribute, value)
185
+ add_log_entry(proxy,
186
+ attribute: attribute,
187
+ method: method,
188
+ kind: 'attribute_read',
189
+ value: value,
190
+ **options)
191
+
192
+ Petra.logger.info "Logged attribute read (#{attribute} => #{value})", :yellow
193
+ true
194
+ end
195
+
196
+ #
197
+ # Logs the initialization of an object
198
+ #
199
+ def log_object_initialization(proxy, method: nil)
200
+ # Mark this object as recently initialized
201
+ recently_initialized_object!(proxy)
202
+
203
+ add_log_entry(proxy,
204
+ kind: 'object_initialization',
205
+ method: method)
206
+ true
207
+ end
208
+
209
+ #
210
+ # Logs the persistence of an object. This basically means that the attribute updates were
211
+ # written to a shared memory. This might simply be the process memory for normal ruby objects,
212
+ # but might also be a call to save() or update() for ActiveRecord::Base instances.
213
+ #
214
+ # @param [Petra::Components::ObjectProxy] proxy
215
+ # The proxy which received the method call
216
+ #
217
+ # @param [String, Symbol] method
218
+ # The method which caused the persistence change
219
+ #
220
+ def log_object_persistence(proxy, method: nil, args: [])
221
+ # All log entries for the current object prior to this persisting method
222
+ # have to be persisted as the object itself is.
223
+ # This includes the object initialization log entry
224
+ log_entries.for_proxy(proxy).each(&:mark_as_object_persisted!)
225
+
226
+ # All attribute reads prior to this have to be persisted
227
+ # as they might have had impact on the current object state.
228
+ # This does not only include the current object, but everything that was
229
+ # read until now!
230
+ # TODO: Could this be more intelligent?
231
+ log_entries.of_kind(:attribute_read).each(&:mark_as_object_persisted!)
232
+
233
+ add_log_entry(proxy,
234
+ method: method,
235
+ kind: 'object_persistence',
236
+ object_persisted: true,
237
+ args: args)
238
+
239
+ true
240
+ end
241
+
242
+ #
243
+ # Logs the destruction of an object.
244
+ # Currently, this is only used with ActiveRecord::Base instances, but there might
245
+ # be a way to handle GC with normal ruby objects (attach a handler to at least get notified).
246
+ #
247
+ def log_object_destruction(proxy, method: nil)
248
+ # Destruction is a form of persistence, resp. its opposite.
249
+ # Therefore, we have to make sure that any other log entries for this
250
+ # object will be transaction persisted as the may have lead to the object's destruction.
251
+ #
252
+ # Currently, this happens even if the object hasn't been persisted prior to
253
+ # its destruction which is accepted behaviour e.g. by ActiveRecord instances.
254
+ # We'll have to see if this should stay the common behaviour.
255
+ log_entries.for_proxy(proxy).each(&:mark_as_object_persisted!)
256
+
257
+ # As for attribute persistence, every attribute which was read in the current section
258
+ # might have had impact on the destruction of this object. Therefore, we have
259
+ # to make sure that all these log entries will be persisted.
260
+ log_entries.of_kind(:attribute_read).each(&:mark_as_object_persisted!)
261
+
262
+ add_log_entry(proxy,
263
+ kind: 'object_destruction',
264
+ method: method,
265
+ object_persisted: true)
266
+ true
267
+ end
268
+
269
+ #
270
+ # Logs the fact that the user decided to ignore further ReadIntegrityErrors
271
+ # on the given attribute as long as its external value stays the same.
272
+ #
273
+ # @param [Boolean] update_value
274
+ # If +true+, a new read set entry is generated along with the RIO one.
275
+ # This will cause the transaction to display the new external value instead of the
276
+ # one we last read and will also automatically invalidate the RIO entry which
277
+ # is only kept to have the whole transaction time line.
278
+ #
279
+ def log_read_integrity_override(proxy, attribute:, external_value:, update_value: false)
280
+ add_log_entry(proxy,
281
+ kind: 'read_integrity_override',
282
+ attribute: attribute,
283
+ external_value: external_value)
284
+
285
+ # If requested, add a new read log entry for the new external value
286
+ log_attribute_read(proxy, attribute: attribute, value: external_value, persist_on_retry: true) if update_value
287
+ end
288
+
289
+ #
290
+ # Logs the fact that the user decided to "undo" all previous changes
291
+ # made to the given attribute
292
+ #
293
+ def log_attribute_change_veto(proxy, attribute:, external_value:)
294
+ add_log_entry(proxy,
295
+ kind: 'attribute_change_veto',
296
+ attribute: attribute,
297
+ external_value: external_value)
298
+
299
+ # Also log the current external attribute value, so the transaction uses the newest available one
300
+ log_attribute_read(proxy, attribute: attribute, value: external_value, persist_on_retry: true)
301
+ end
302
+
303
+ #----------------------------------------------------------------
304
+ # Object Handling
305
+ #----------------------------------------------------------------
306
+
307
+ #
308
+ # As objects which were initialized inside a transaction receive
309
+ # a temporary ID whose generation again requires knowledge about
310
+ # their membership regarding the below object sets leading to an
311
+ # infinite loop, we have to keep a temporary list of object ids (ruby)
312
+ # until they received their transaction object id
313
+ #
314
+ def recently_initialized_objects
315
+ @recently_initialized_objects ||= []
316
+ end
317
+
318
+ def recently_initialized_object!(proxy)
319
+ recently_initialized_objects << proxy.send(:proxied_object).object_id
320
+ end
321
+
322
+ def recently_initialized_object?(proxy)
323
+ recently_initialized_objects.include?(proxy.send(:proxied_object).object_id)
324
+ end
325
+
326
+ #
327
+ # @return [Hash<Petra::Proxies::ObjectProxy, Array<String,Symbol>>]
328
+ # All attributes which were read during this section grouped by the objects (proxies)
329
+ # they belong to.
330
+ #
331
+ # Only entries which were previously marked as object persisted are taken into account.
332
+ #
333
+ def read_attributes
334
+ cache_if_persisted(:read_attributes) do
335
+ log_entries.of_kind(:attribute_read).object_persisted.each_with_object({}) do |entry, h|
336
+ h[entry.load_proxy] ||= []
337
+ h[entry.load_proxy] << entry.attribute unless h[entry.load_proxy].include?(entry.attribute)
338
+ end
339
+ end
340
+ end
341
+
342
+ #
343
+ # @return [Array<Petra::Proxies::ObjectProxy>] All Objects that were part of this section.
344
+ # Only log entries marked as object persisted are taken into account
345
+ #
346
+ def objects
347
+ cache_if_persisted(:all_objects) do
348
+ log_entries.object_persisted.map(&:load_proxy).uniq
349
+ end
350
+ end
351
+
352
+ #
353
+ # @return [Array<Petra::Proxies::ObjectProxy>] Objects that were read during this section
354
+ # Only read log entries which were marked as object persisted are taken into account
355
+ #
356
+ def read_objects
357
+ cache_if_persisted(:read_objects) do
358
+ read_attributes.keys
359
+ end
360
+ end
361
+
362
+ #
363
+ # @return [Array<Petra::Proxies::ObjectProxy>] Objects that were created during this section.
364
+ #
365
+ # It does not matter whether the section was persisted or not in this case,
366
+ # the only condition is that the object was "object_persisted" after its initialization
367
+ #
368
+ def created_objects
369
+ cache_if_persisted(:created_objects) do
370
+ log_entries.of_kind(:object_initialization).object_persisted.map(&:load_proxy).uniq
371
+ end
372
+ end
373
+
374
+ #
375
+ # @return [Array<Petra::Proxies::ObjectProxy>] Objects which were initialized, but not
376
+ # yet persisted during this section. This may only be the case for the current section
377
+ #
378
+ def initialized_objects
379
+ cache_if_persisted(:initialized_objects) do
380
+ log_entries.of_kind(:object_initialization).not_object_persisted.map(&:load_proxy).uniq
381
+ end
382
+ end
383
+
384
+ #
385
+ # @see #created_objects
386
+ #
387
+ # This method will also return objects which were not yet `object_persisted`, e.g.
388
+ # to be used during the current transaction section
389
+ #
390
+ def initialized_or_created_objects
391
+ cache_if_persisted(:initialized_or_created_objects) do
392
+ (initialized_objects + created_objects).uniq
393
+ end
394
+ end
395
+
396
+ #
397
+ # @return [Array<Petra::Proxies::ObjectProxies>] Objects which were destroyed
398
+ # during the current section
399
+ #
400
+ def destroyed_objects
401
+ cache_if_persisted(:destroyed_objects) do
402
+ log_entries.of_kind(:object_destruction).map(&:load_proxy).uniq
403
+ end
404
+ end
405
+
406
+ #----------------------------------------------------------------
407
+ # Persistence
408
+ #----------------------------------------------------------------
409
+
410
+ #
411
+ # Removes all log entries and empties the read and write set.
412
+ # This should only be done on the current section and as long as the log
413
+ # entries haven't been persisted.
414
+ #
415
+ def reset!
416
+ fail Petra::PetraError, 'An already persisted section may not be reset' if persisted?
417
+ @log_entries = []
418
+ @read_set = []
419
+ @write_set = []
420
+ end
421
+
422
+ def prepare_for_retry!
423
+ log_entries.prepare_for_retry!
424
+ end
425
+
426
+ #
427
+ # @see Petra::Components::EntrySet#apply
428
+ #
429
+ def apply_log_entries!
430
+ log_entries.apply!
431
+ end
432
+
433
+ #
434
+ # @see Petra::Components::EntrySet#enqueue_for_persisting!
435
+ #
436
+ def enqueue_for_persisting!
437
+ log_entries.enqueue_for_persisting!
438
+ @persisted = true
439
+ end
440
+
441
+ private
442
+
443
+ #
444
+ # Executes the block and caches its result if the current section has already
445
+ # been persisted (= won't change any more)
446
+ #
447
+ def cache_if_persisted(name)
448
+ @cache ||= {}
449
+ return (@cache[name.to_s] ||= yield) if persisted?
450
+ yield
451
+ end
452
+
453
+ #
454
+ # Adds a new log entry to the current section.
455
+ # New log entries are not automatically persisted, this is done through #enqueue_for_persisting!
456
+ #
457
+ # @param [Petra::Components::ObjectProxy] proxy
458
+ #
459
+ # @param [Boolean] object_persisted
460
+ #
461
+ # @param [Hash] options
462
+ #
463
+ def add_log_entry(proxy, kind:, object_persisted: false, **options)
464
+ attribute = options.delete(:attribute)
465
+ attribute_key = attribute && proxy.__attribute_key(attribute)
466
+
467
+ Petra::Components::LogEntry.log!(kind,
468
+ section: self,
469
+ transaction_identifier: transaction.identifier,
470
+ savepoint: savepoint,
471
+ attribute_key: attribute_key,
472
+ object_key: proxy.__object_key,
473
+ object_persisted: object_persisted,
474
+ transaction_persisted: persisted?,
475
+ new_object: proxy.__new?,
476
+ **options).tap do |entry|
477
+ Petra.logger.debug "Added Log Entry: #{entry}", :yellow
478
+ log_entries << entry
479
+ end
480
+ end
481
+
482
+ #
483
+ # In case the this section is not the latest one in the current transaction,
484
+ # we have to load the steps previously done from the persistence value
485
+ #
486
+ # Also sets the `persisted?` flag depending on whether this section has
487
+ # be previously persisted or not.
488
+ #
489
+ def load_persisted_log_entries
490
+ @log_entries = EntrySet.new(Petra.transaction_manager.persistence_adapter.log_entries(self))
491
+ @log_entries.each do |entry|
492
+ if entry.kind?(:attribute_change)
493
+ write_set[entry.attribute_key] = entry.new_value
494
+ elsif entry.kind?(:attribute_read)
495
+ read_set[entry.attribute_key] = entry.value
496
+ elsif entry.kind?(:read_integrity_override)
497
+ read_integrity_overrides[entry.attribute_key] = entry.external_value
498
+ elsif entry.kind?(:attribute_change_veto)
499
+ attribute_change_vetoes[entry.attribute_key] = entry.external_value
500
+ # Remove any value changes done to the attribute previously in this section
501
+ # This will speed up finding active attribute change vetoes as
502
+ # the search is already canceled if no write set entry exists.
503
+ write_set.delete(entry.attribute_key)
504
+ end
505
+ end
506
+
507
+ @persisted = @log_entries.any?
508
+ end
509
+
510
+ def proxied_object(proxy)
511
+ proxy.send(:proxied_object)
512
+ end
513
+
514
+ #
515
+ # Sets a new value for the given attribute in this section's write set
516
+ #
517
+ def add_to_write_set(proxy, attribute, value)
518
+ write_set[proxy.__attribute_key(attribute)] = value
519
+ end
520
+
521
+ #
522
+ # @see #add_to_write_set
523
+ #
524
+ def add_to_read_set(proxy, attribute, value)
525
+ read_set[proxy.__attribute_key(attribute)] = value
526
+ end
527
+
528
+ #
529
+ # Builds the next savepoint name based on the transaction identifier and a version number
530
+ #
531
+ def next_savepoint_name
532
+ version = if transaction.sections.empty?
533
+ 1
534
+ else
535
+ transaction.sections.last.savepoint_version + 1
536
+ end
537
+
538
+ [transaction.identifier, version.to_s].join('/')
539
+ end
540
+
541
+ end
542
+ end
543
+ end