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,342 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'petra/util/field_accessors'
4
+ require 'petra/util/registrable'
5
+
6
+ module Petra
7
+ module Components
8
+ #
9
+ # A log entry is basically a struct with certain helper functions.
10
+ # This class contains some base functionality and a map of more specific log entry types.
11
+ #
12
+ # Registered entry types may define own +field_accessors+ which will be
13
+ # serialized when persisting the log entries.
14
+ #
15
+ class LogEntry
16
+ include Comparable
17
+ include Petra::Util::Registrable
18
+ include Petra::Util::FieldAccessors
19
+
20
+ acts_as_register :entry_type
21
+
22
+ field_accessor :savepoint
23
+ field_accessor :transaction_identifier
24
+
25
+ # Identifier usually used to persist the current entry.
26
+ # It is set by the used persistence adapter.
27
+ field_accessor :entry_identifier
28
+
29
+ # An object is a 'new object' if it was created during this transaction and does
30
+ # not exist outside of it yet. This information is necessary when restoring
31
+ # proxies from previous sections
32
+ field_accessor :new_object
33
+
34
+ # Identifies the object the changes were performed on,
35
+ # e.g. "User", 1 for @user.save
36
+ # The object class is also needed to load the corresponding class configuration
37
+ attr_reader :object_class
38
+ attr_reader :attribute
39
+
40
+ field_accessor :object_key
41
+ field_accessor :attribute_key
42
+
43
+ # Means that the client persisted the object referenced by this log entry, e.g. through #save in case of AR
44
+ attr_accessor :object_persisted
45
+
46
+ # Marks this log entry to be persisted on section retries even if the object was not persisted
47
+ attr_writer :persist_on_retry
48
+
49
+ # Means that the log entry was actually persisted within the transaction
50
+ attr_accessor :transaction_persisted
51
+
52
+ attr_reader :section
53
+
54
+ alias object_persisted? object_persisted
55
+ alias transaction_persisted? transaction_persisted
56
+ alias new_object? new_object
57
+
58
+ def self.log!(kind, section:, **fields)
59
+ fail ArgumentError, "#{kind} is not a valid entry type" unless registered_entry_type?(kind)
60
+ registered_entry_type(kind).new(section, **fields)
61
+ end
62
+
63
+ #
64
+ # Initializes a new log entry based on the given section and options
65
+ #
66
+ # @param [Hash] fields
67
+ # @option options [String] :savepoint (section.savepoint)
68
+ # The savepoint name this log entry is part of.
69
+ #
70
+ # @option options [String] :transaction_identifier (section.transaction.identifier)
71
+ # The entry's transaction's identifier
72
+ #
73
+ # @option options [String, Symbol] :method
74
+ # The method which caused this log entry.
75
+ # In some cases, it makes sense to specify a different method here as
76
+ # this value is used when actually applying the log entry (see Petra::Proxies::ActiveRecordProxy)
77
+ #
78
+ # @option options [String] attribute_key
79
+ # The unique key for the attribute which was altered/read in this log entry.
80
+ # Only available for attribute_read/attribute_change entries
81
+ #
82
+ # @option options [String] object_key
83
+ # The unique key for the object/proxy which caused this log entry.
84
+ # The internal values for @object_class and @object_id are automatically set based on it.
85
+ #
86
+ # @option options [String] kind
87
+ # The entry's kind, e.g. 'object_initialization'
88
+ #
89
+ # @option options [String] old_value
90
+ # @option options [String] new_value
91
+ #
92
+ def initialize(section, **fields)
93
+ @section = section
94
+
95
+ @object_persisted = fields.delete(:object_persisted)
96
+ @transaction_persisted = fields.delete(:transaction_persisted)
97
+
98
+ # Restore the given field accessors
99
+ fields.each do |k, v|
100
+ send("#{k}=", v)
101
+ end
102
+
103
+ self.savepoint ||= section.savepoint
104
+ self.transaction_identifier ||= section.transaction.identifier
105
+ end
106
+
107
+ #
108
+ # If both entries were made in the same section, the smaller entry was
109
+ # generated earlier than the other.
110
+ # If both entries are in different sections, the one with a smaller
111
+ # savepoint version is considered smaller.
112
+ #
113
+ def <=>(other)
114
+ if section == other.section
115
+ section.log_entries.index(self) <=> section.log_entries.index(other)
116
+ else
117
+ section.savepoint_version <=> other.section.savepoint_version
118
+ end
119
+ end
120
+
121
+ #----------------------------------------------------------------
122
+ # Internal Field Handling
123
+ #----------------------------------------------------------------
124
+
125
+ def attribute_change?
126
+ kind?(:attribute_change)
127
+ end
128
+
129
+ def attribute_read?
130
+ kind?(:attribute_read)
131
+ end
132
+
133
+ def object_persistence?
134
+ kind?(:object_persistence)
135
+ end
136
+
137
+ def object_initialization?
138
+ kind?(:object_initialization)
139
+ end
140
+
141
+ def mark_as_object_persisted!
142
+ @object_persisted = true
143
+ end
144
+
145
+ def mark_as_persisted!(identifier)
146
+ @transaction_persisted = true
147
+ @entry_identifier = identifier
148
+ end
149
+
150
+ #
151
+ # @return [Boolean] +true+ if this log entry should be destroyed
152
+ # if it is enqueued for the next persisting phase
153
+ #
154
+ def marked_for_destruction?
155
+ !!@marked_for_destruction
156
+ end
157
+
158
+ #
159
+ # @return [Hash] the necessary information about this entry to reproduce it later
160
+ # The result is mainly used when serializing the step later.
161
+ #
162
+ # @param [Hash] options
163
+ #
164
+ # @option options [String] :entry_identifier
165
+ # A section-unique identifier for the current log entry.
166
+ # It is usually given by the used persistence adapter.
167
+ #
168
+ # Information about the object / transaction persistence is not kept as this method
169
+ # will only be used during persistence or on already persisted entries
170
+ #
171
+ def to_h(**options)
172
+ fields.each_with_object(options.merge('kind' => self.class.kind)) do |(k, v), h|
173
+ h[k] = v unless v.nil?
174
+ end
175
+ end
176
+
177
+ #
178
+ # Builds a log entry from the given section and hash, but automatically sets the persistence flags
179
+ #
180
+ # @return [Petra::Components::LogEntry]
181
+ #
182
+ def self.from_hash(section, fields)
183
+ log!(fields.delete('kind'),
184
+ section: section,
185
+ object_persisted: true,
186
+ transaction_persisted: true,
187
+ **fields.symbolize_keys)
188
+ end
189
+
190
+ #
191
+ # @return [Boolean] +true+ if this log entry was made in the context of the given object (key)
192
+ #
193
+ def for_object?(object_key)
194
+ self.object_key == object_key
195
+ end
196
+
197
+ #
198
+ # @return [Boolean] +true+ if this log entry is of the given kind
199
+ #
200
+ def kind?(kind)
201
+ self.class.kind.to_s == kind.to_s
202
+ end
203
+
204
+ #----------------------------------------------------------------
205
+ # Persistence
206
+ #----------------------------------------------------------------
207
+
208
+ #
209
+ # Adds the log entry to the persistence queue if the following conditions are met:
210
+ #
211
+ # 1. The log entry has to be marked as 'object_persisted', meaning that the object was saved
212
+ # during/after the action which created the the entry
213
+ # 2. The log entry hasn't been persisted previously
214
+ #
215
+ # This does not automatically mark this log entry as persisted,
216
+ # this is done once the persistence adapter finished its work
217
+ #
218
+ def enqueue_for_persisting!
219
+ return if transaction_persisted?
220
+ return unless persist? || persist_on_retry? && transaction.retry_in_progress?
221
+ Petra.transaction_manager.persistence_adapter.enqueue(self)
222
+ end
223
+
224
+ #
225
+ # @return [boolean] If this returns +true+, the log entry will be
226
+ # persisted when a section is retried even if #persist? would return +false+
227
+ #
228
+ def persist_on_retry?
229
+ !!@persist_on_retry
230
+ end
231
+
232
+ #
233
+ # May be overridden by more specialized log entries,
234
+ # the basic version will persist an entry as long as it is marked
235
+ # as object persisted
236
+ #
237
+ def persist?
238
+ object_persisted?
239
+ end
240
+
241
+ #----------------------------------------------------------------
242
+ # Commit
243
+ #----------------------------------------------------------------
244
+
245
+ #
246
+ # Applies the action performed in the current log entry
247
+ # to the corresponding object
248
+ #
249
+ def apply!
250
+ not_implemented # nop?
251
+ end
252
+
253
+ #
254
+ # Tries to undo a previously done #apply!
255
+ # This is currently only possible for attribute changes as we do not know
256
+ # how to undo destruction / persistence for general objects
257
+ #
258
+ def undo!
259
+ load_proxy.send(:__undo_application__, self)
260
+ end
261
+
262
+ #----------------------------------------------------------------
263
+ # Object Helpers
264
+ #----------------------------------------------------------------
265
+
266
+ #
267
+ # @return [Petra::Proxies::ObjectProxy] the proxy this log entry was made for
268
+ #
269
+ def load_proxy
270
+ @load_proxy ||= transaction.objects.fetch(object_key) do
271
+ new_object? ? initialize_proxy : restore_proxy
272
+ end
273
+ end
274
+
275
+ def to_s
276
+ "#{section.savepoint}/#{@object_id} => #{self.class.kind}"
277
+ end
278
+
279
+ protected
280
+
281
+ def proxied_object
282
+ load_proxy.send(:proxied_object)
283
+ end
284
+
285
+ #
286
+ # Initializes a proxy for the object which was initialized in this log entry.
287
+ # This is done by initializing a new object and set the old generated object_id for its proxy
288
+ #
289
+ # @return [Petra::Proxies::ObjectProxy] a proxy for a clean object (no attributes set).
290
+ # Every attribute value is taken from its write set.
291
+ #
292
+ def initialize_proxy
293
+ klass = object_class.constantize
294
+ instance = configurator.__inherited_value(:init_method, proc_expected: true, base: klass)
295
+ Petra::Proxies::ObjectProxy.for(instance, object_id: @object_id)
296
+ end
297
+
298
+ #
299
+ # Loads an object which most likely existed outside of the transaction
300
+ # and wraps it in a proxy.
301
+ #
302
+ # @return [Petra::Proxies::ObjectProxy] a proxy for the object this log entry is about.
303
+ #
304
+ # Please note that no custom attributes are set, they will be served from the write set.
305
+ #
306
+ # TODO: Raise an exception here if a proxy could not be restored.
307
+ # This most likely means that the object was destroyed outside of the transaction!
308
+ #
309
+ def restore_proxy
310
+ klass = object_class.constantize
311
+ instance = configurator.__inherited_value(:lookup_method, @object_id, proc_expected: true, base: klass)
312
+ Petra::Proxies::ObjectProxy.for(instance)
313
+ end
314
+
315
+ def object_key=(key)
316
+ self[:object_key] = key
317
+ @object_class, @object_id = key.split('/') if key
318
+ end
319
+
320
+ def attribute_key=(key)
321
+ self[:attribute_key] = key
322
+ @object_class, @object_id, @attribute = key.split('/') if key
323
+ end
324
+
325
+ def configurator
326
+ @configurator ||= Petra.configuration[object_class]
327
+ end
328
+
329
+ def transaction
330
+ Petra.current_transaction
331
+ end
332
+
333
+ #
334
+ # Ensures that the currently set log entry kind is actually one of the valid ones
335
+ #
336
+ def validate_kind!
337
+ return if Petra::Components::LogEntry.registered_entry_type?(kind)
338
+ fail ArgumentError, "#{kind} is not a valid log entry kind."
339
+ end
340
+ end
341
+ end
342
+ end
@@ -0,0 +1,209 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Petra
4
+ module Components
5
+ #
6
+ # This class encapsulates the methods a transaction may use to
7
+ # gather information about the objects which were used during its execution.
8
+ #
9
+ # It also functions as an object cache, mapping proxy keys to (temporary) object proxies
10
+ #
11
+ class ProxyCache
12
+ def initialize(transaction)
13
+ @transaction = transaction
14
+ end
15
+
16
+ #
17
+ # Returns the proxy for the given object key from cache.
18
+ # If there is no valid object cached yet, the given block is executed
19
+ # and its result saved under the given key.
20
+ #
21
+ # @return [Petra::Proxies::ObjectProxy] the object proxy by the given object key
22
+ #
23
+ def fetch(key)
24
+ @cache ||= {}
25
+ return @cache[key.to_s] if @cache.key?(key.to_s)
26
+ fail ArgumentError, "Object `#{key}` is not cached and no block was given." unless block_given?
27
+ @cache[key.to_s] = yield
28
+ end
29
+
30
+ # Shortcut to retrieve already cached objects.
31
+ # As #[] may not receive a block, it will automatically fail if the
32
+ # cache entry couldn't be found.
33
+ alias [] fetch
34
+
35
+ delegate :sections, :current_section, :verify_attribute_integrity!, to: :@transaction
36
+
37
+ #
38
+ # @return [Hash<Petra::Proxies::ObjectProxy, Array<String,Symbol>>]
39
+ # All attributes which were read during this transaction grouped by the objects (proxies)
40
+ # they belong to.
41
+ #
42
+ def read_attributes
43
+ sections.each_with_object({}) do |section, h|
44
+ section.read_attributes.each do |proxy, attributes|
45
+ h[proxy] ||= []
46
+ h[proxy] = (h[proxy] + attributes).uniq
47
+ end
48
+ end
49
+ end
50
+
51
+ #
52
+ # Objects that will have impact during the commit phase in order of their appearance
53
+ # during the transaction's execution.
54
+ #
55
+ def fateful(klass = nil)
56
+ filtered_objects(:objects, klass)
57
+ end
58
+
59
+ #
60
+ # Objects that were read during the transaction
61
+ #
62
+ # @param [Class] klass
63
+ #
64
+ # @return [Array<Petra::Proxies::ObjectProxy>]
65
+ #
66
+ def read(klass = nil)
67
+ filtered_objects(:read_objects, klass)
68
+ end
69
+
70
+ #
71
+ # Objects that were initialized and persisted during the transaction
72
+ #
73
+ # @param [Class] klass
74
+ #
75
+ # @return [Array<Petra::Proxies::ObjectProxy>]
76
+ #
77
+ def created(klass = nil)
78
+ filtered_objects(:created_objects, klass)
79
+ end
80
+
81
+ #
82
+ # Like #created, but it will also include not-yet persisted objects
83
+ # of non-persisted sections
84
+ #
85
+ # @param [Class] klass
86
+ #
87
+ # @return [Array<Petra::Proxies::ObjectProxy>]
88
+ #
89
+ def initialized_or_created(klass = nil)
90
+ filtered_objects(:initialized_or_created_objects, klass)
91
+ end
92
+
93
+ #
94
+ # @see #filtered_objects
95
+ #
96
+ # @param [Class] klass
97
+ #
98
+ # @return [Array<Petra::Proxies::ObjectProxy>] objects which were initialized within the transaction,
99
+ # but not yet object persisted
100
+ #
101
+ def initialized(klass = nil)
102
+ filtered_objects(:initialized_objects, klass)
103
+ end
104
+
105
+ #
106
+ # @see #filtered_objects
107
+ #
108
+ # @param [Class] klass
109
+ #
110
+ # @return [Array<Petra::Proxies::ObjectProxy>] objects which were destroyed within the transaction
111
+ #
112
+ def destroyed(klass = nil)
113
+ filtered_objects(:destroyed_objects, klass)
114
+ end
115
+
116
+ #
117
+ # @param [Petra::Proxies::ObjectProxy] proxy
118
+ #
119
+ # @return [Boolean] +true+ if the given object (proxy) was initialized AND persisted during the transaction.
120
+ #
121
+ def created?(proxy)
122
+ created.include?(proxy)
123
+ end
124
+
125
+ #
126
+ # @param [Petra::Proxies::ObjectProxy] proxy
127
+ #
128
+ # @return [Boolean] +true+ if the given object (proxy) was initialized, but not yet persisted
129
+ # during this transaction. This means in particular that the object did not exist before
130
+ # the transaction started.
131
+ #
132
+ def initialized?(proxy)
133
+ initialized.include?(proxy)
134
+ end
135
+
136
+ #
137
+ # @param [Petra::Proxies::ObjectProxy]
138
+ #
139
+ # @return [Boolean] +true+ if the given object did not exist outside of the transaction,
140
+ # meaning that it was initialized and optionally persisted during its execution
141
+ #
142
+ def new?(proxy)
143
+ current_section.recently_initialized_object?(proxy) || initialized_or_created.include?(proxy)
144
+ end
145
+
146
+ #
147
+ # @param [Petra::Proxies::ObjectProxy] proxy
148
+ #
149
+ # @return [Boolean] +true+ if the given object existed before the transaction started
150
+ #
151
+ def existing?(proxy)
152
+ !new?(proxy)
153
+ end
154
+
155
+ #
156
+ # @param [Petra::Proxies::ObjectProxy] proxy
157
+ #
158
+ # @return [Boolean] +true+ if the given object was destroyed during this transaction
159
+ #
160
+ def destroyed?(proxy)
161
+ destroyed.include?(proxy)
162
+ end
163
+
164
+ #----------------------------------------------------------------
165
+ # Helpers
166
+ #----------------------------------------------------------------
167
+
168
+ def current_numerical_id
169
+ # FIXME: The string comparison will not work for numbers > 10! It has to be replaced with a numeric comparison!
170
+ # FIXME: This also causes problems when the last new element is deleted and a new one created
171
+ @current_numerical_id ||= (initialized_or_created.max_by(&:__object_id)&.__object_id || 'new_0')
172
+ .match(/new_(\d+)/)[1].to_i
173
+ end
174
+
175
+ def inc_current_numerical_id
176
+ @current_numerical_id = current_numerical_id + 1
177
+ end
178
+
179
+ def next_id
180
+ format('new_%05d', inc_current_numerical_id)
181
+ end
182
+
183
+ #
184
+ # Performs an integrity check on all attributes which were read in this transaction
185
+ #
186
+ # @raise [Petra::ReadIntegrityError] Raised if one of the read attribute has be changed externally
187
+ #
188
+ def verify_read_attributes!(force: false)
189
+ read_attributes.each do |proxy, attributes|
190
+ attributes.each { |a| verify_attribute_integrity!(proxy, attribute: a, force: force) }
191
+ end
192
+ end
193
+
194
+ private
195
+
196
+ #
197
+ # Collects objects of a certain kind from all sections and filters by a given class name (optionally)
198
+ # There is no caching here as both, sections and log entries will cache the actual objects/sets
199
+ #
200
+ def filtered_objects(kind, klass = nil)
201
+ result = sections.flat_map(&kind)
202
+
203
+ # If a class (name) was given, only return objects which are of the given type
204
+ klass ? result.select { |p| p.send(:for_class?, klass) } : result
205
+ end
206
+
207
+ end
208
+ end
209
+ end