petra_core 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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