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,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