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
data/README.md ADDED
@@ -0,0 +1,726 @@
1
+ [![Build Status](https://travis-ci.org/Stex/petra.svg?branch=master)](https://travis-ci.org/Stex/petra)
2
+
3
+ # petra
4
+ <img src="https://drive.google.com/uc?id=1BKauBWbE66keL1gBBDfgSaRE0lL5x586&export=download" width="200" align="right" />
5
+
6
+ Petra is a proof-of-concept for **pe**rsisted **tra**nsactions in Ruby with (hopefully) full ACI(D) properties.
7
+
8
+ Please note that this was created during my master's thesis in 2016 and hasn't been extended a lot since then except for a few coding style fixes. I would write a lot of stuff differently today, but the main concept is still interesting enough.
9
+
10
+ It allows starting a transaction without committing it and resuming it at a later time, even in another process - given the used objects provide identifiers other than `object_id`.
11
+
12
+ It should work with every Ruby object and can be extended to work with web frameworks like Ruby-on-Rails as well (a POC of RoR integration can be found at [stex/petra-rails](https://github.com/stex/petra-rails)).
13
+
14
+ This README only covers parts of what `petra` has to offer. Feel free to dive into the code, everything should be commented accordingly.
15
+
16
+ Let's take a look at how `petra` is used:
17
+
18
+ ```ruby
19
+ class SimpleUser
20
+ attr_accessor :first_name, :last_name
21
+
22
+ def name
23
+ "#{first_name} #{last_name}"
24
+ end
25
+
26
+ # ... configuration, see below
27
+ end
28
+
29
+ user = SimpleUser.petra.new('John', 'Doe')
30
+
31
+ # Start a new transaction and start changing attributes
32
+ Petra.transaction(identifier: 'tr1') do
33
+ user.first_name = 'Foo'
34
+ end
35
+
36
+ # No changes outside the transaction yet...
37
+ puts user.name #=> 'John Doe'
38
+
39
+ # Continue the same transaction
40
+ Petra.transaction(identifier: 'tr1') do
41
+ puts user.name #=> 'Foo Doe'
42
+ user.last_name = 'Bar'
43
+ end
44
+
45
+ # Another transaction changes a value already changed in 'tr1'
46
+ Petra.transaction do
47
+ user.first_name = 'Moo'
48
+ Petra.commit!
49
+ end
50
+
51
+ puts user.name #=> 'Moo Doe'
52
+
53
+ # Try to commit our first transaction
54
+ Petra.transaction(identifier: 'tr1') do
55
+ puts user.name
56
+ Petra.commit!
57
+ rescue Petra::WriteClashError => e
58
+ # => "The attribute `first_name` has been changed externally and in the transaction. (Petra::WriteClashError)"
59
+ # Let's use our value and go on with committing the transaction
60
+ e.use_ours!
61
+ e.continue!
62
+ end
63
+
64
+ # The actual object is updated with the values from tr1
65
+ puts user.name #=> 'Foo Bar'
66
+ ```
67
+
68
+ We just used a simple Ruby object inside a transaction which was even split into multiple sections!
69
+
70
+ (The full example can be found at [`examples/showcase.rb`](https://github.com/Stex/petra/blob/master/examples/showcase.rb))
71
+
72
+ <!-- START doctoc generated TOC please keep comment here to allow auto update -->
73
+ <!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
74
+ ## TOC
75
+
76
+ - [Basic Usage](#basic-usage)
77
+ - [Starting/Resuming a transaction](#startingresuming-a-transaction)
78
+ - [Transactional Objects and their Configuration](#transactional-objects-and-their-configuration)
79
+ - [Commit / Rollback / Reset / Retry](#commit--rollback--reset--retry)
80
+ - [Reacting to external changes](#reacting-to-external-changes)
81
+ - [An attribute we previously read was changed externally](#an-attribute-we-previously-read-was-changed-externally)
82
+ - [An attribute we changed in our transaction was also changed externally](#an-attribute-we-changed-in-our-transaction-was-also-changed-externally)
83
+ - [`continue!`?](#continue)
84
+ - [Full Configuration Options](#full-configuration-options)
85
+ - [Global Options](#global-options)
86
+ - [Class Specific Options](#class-specific-options)
87
+ - [Extending `petra`](#extending-petra)
88
+ - [Class Proxies](#class-proxies)
89
+ - [Module Proxies](#module-proxies)
90
+ - [Persistence Adapters](#persistence-adapters)
91
+
92
+ <!-- END doctoc generated TOC please keep comment here to allow auto update -->
93
+
94
+ ## Installation
95
+
96
+ Simply add the following line to your gemfile:
97
+
98
+ ```ruby
99
+ gem 'petra_core', require: 'petra'
100
+ ```
101
+
102
+ Unfortunately, the gem name `petra` is already taken and `petra-core` would express that this gem is extending it, so
103
+ I went for an underscore for now. It's hard finding nice-sounding gem names which are not yet taken nowadays :/
104
+
105
+ ## Basic Usage
106
+
107
+ ### Starting/Resuming a transaction
108
+
109
+ Whenver you call `Petra.transaction`, a *transaction section* is started. If you pass in an identifier and a matching transaction already exists, it will be resumed instead.
110
+
111
+ ```ruby
112
+ # Starting a new transaction with an auto-generated identifier
113
+ tr_id = Petra.transaction {}
114
+
115
+ # Resuming the transaction
116
+ Petra.transaction(identifier: tr_id) {}
117
+ ```
118
+
119
+ ### Transactional Objects and their Configuration
120
+
121
+ Although `petra` is seemingly able to use every Ruby object inside a transaction, it does not patch these objects in any way by e.g. overriding their getters and setters. Instead, a transparent proxy is used:
122
+
123
+ ```
124
+ # Normal instance of SimpleUser
125
+ user = SimpleUser.new
126
+
127
+ # ObjectProxy, can now be used inside and outside of transactions
128
+ user = SimpleUser.petra.new # or: user = SimpleUser.new.petra
129
+ ```
130
+
131
+ In its current version, `petra` has to be told about the meaning of the different methods of a class to be used inside a transaction.
132
+ This decision was made as there are no strict conventions regarding method names in Ruby (e.g. `getX`/`setX` in Java).
133
+
134
+ `petra` knows about 5 different kinds of methods:
135
+
136
+ 1. **Attribute Readers** which retrieve a current attribute value
137
+ 2. **Attribute Writers** which set a new attribute value
138
+ 3. **Dynamic Attribute Readers** which a composite methods like `name` (not an actual attribute, but use attributes interally)
139
+ 4. **Persistence Methods** which save changes made to the object (think of `ActiveRecord::Base#save`)
140
+ 5. **Destruction Methods** which remove the object
141
+
142
+
143
+ Let's create a configuration for `SimpleUser`:
144
+
145
+ ```ruby
146
+ Petra.configure do
147
+ configure_class SimpleUser do
148
+ # Tell petra about our available attribute readers
149
+ attribute_reader? do |method_name|
150
+ %w[first_name last_name].include?(method_name.to_s)
151
+ end
152
+
153
+ # Do the same for attribute writers
154
+ attribute_writer? do |method_name|
155
+ %w[first_name= last_name=].include?(method_name.to_s)
156
+ # also possible here: `method_name.last == '='`
157
+ end
158
+
159
+ # Define which methods are used to persist instances of SimpleUser
160
+ persistence_method? do |method_name|
161
+ %w[first_name= last_name=].include?(method_name.to_s)
162
+ end
163
+
164
+ # `name` uses attributes internally
165
+ dynamic_attribute_reader? do |method_name|
166
+ %[name].include?(method_name.to_s)
167
+ end
168
+ end
169
+ end
170
+ ```
171
+
172
+ As you may have noticed, we used our `attribute_writer`s twice in this configuration: Once as actual attribute writers and once as persistence method. This was done to keep the example above as small as possible.
173
+
174
+ The same could have been achieved by setting up a no-op method and configuring it accordingly:
175
+
176
+ ```ruby
177
+ # SimpleUser
178
+ def save; end
179
+
180
+ # Configuration
181
+ persistence_method { |method_name| %w[save].include?(method_name.to_s) }
182
+
183
+ # Usage
184
+ Petra.transaction do
185
+ user.first_name = 'Foo'
186
+ user.save
187
+ end
188
+ ```
189
+
190
+ In this case, not calling `save` inside the transaction would have lead to the loss of everything we did inside the transaction section.
191
+
192
+ ### Commit / Rollback / Reset / Retry
193
+
194
+ #### Commit
195
+
196
+ Transactions can be committed by calling `Petra.commit!` inside a `Petra.transaction` block.
197
+ It will leave the transaction block afterwards and not execute anything left in it:
198
+
199
+ ```ruby
200
+ Petra.transaction do
201
+ Petra.commit!
202
+ puts 'I will never be shown!'
203
+ end
204
+ ```
205
+
206
+ #### Rollback
207
+
208
+ A rollback can be triggered by either raising `Petra::Rollback` or simply any other uncaught `StandardError`. The difference is that `Petra::Rollback` will be swallowed by the transaction processing (like `ActiveRecord::Rollback` does), while any other error will be re-raised.
209
+
210
+ Triggering a rollback will undo all changes made **in the current section** of the transaction. All previous sections are not affected.
211
+
212
+ ```ruby
213
+ Petra.transaction(identifier: 'tr1') do
214
+ user.first_name = 'Foo'
215
+ end
216
+
217
+ Petra.transaction(identifier: 'tr1') do
218
+ user.last_name = 'Bar'
219
+ fail Petra::Rollback
220
+ end
221
+ ```
222
+
223
+ In this example, only the change to `user#last_name` is lost.
224
+
225
+ #### Reset
226
+
227
+ A reset can be triggered by raising `Petra::Reset`. It works like a rollback, but will clear **the whole transaction**.
228
+
229
+ ```ruby
230
+ Petra.transaction(identifier: 'tr1') do
231
+ user.first_name = 'Foo'
232
+ end
233
+
234
+ Petra.transaction(identifier: 'tr1') do
235
+ user.last_name = 'Bar'
236
+ fail Petra::Reset
237
+ end
238
+ ```
239
+
240
+ Here, all changes to `user` are lost.
241
+
242
+ #### Retry
243
+
244
+ A retry means that the current transaction block should be retried again after a rollback.
245
+
246
+ ```ruby
247
+ Petra.transaction(identifier: 'tr1') do
248
+ user.last_name = 'Bar'
249
+ fail Petra::Retry if some_condition
250
+ end
251
+ ```
252
+
253
+ ## Reacting to external changes
254
+
255
+ As the transaction is working in isolation on its own data set, it might happen that the original objects outside the transaction are changed in the meantime, e.g. by another transaction's commit:
256
+
257
+ ```ruby
258
+ Petra.transaction(identifier: 'tr1') do
259
+ user.first_name = 'Foo'
260
+ end
261
+
262
+ Petra.transaction(identifier: 'tr2') do
263
+ user.first_name = 'Moo'
264
+ Petra.commit!
265
+ end
266
+
267
+ Petra.transaction(identifier: 'tr1') do
268
+ # we don't know about the external change here and would
269
+ # possibly override it
270
+ end
271
+ ```
272
+
273
+ `petra` reacts to these external changes and raises a corresponding exception. This exception allows the developer to solve the conflicts based on his current context.
274
+
275
+ The exception is thrown either when the attribute is used again or during the commit phase. Not handling any of these exception yourself will result in a transaction reset.
276
+
277
+ Each error described below shares a few common methods to control the further transaction flow:
278
+
279
+ ```ruby
280
+ Petra.transaction(identifier: 'tr1') do
281
+ begin
282
+ ...
283
+ rescue Petra::ValueComparisionError => e # Superclass of ReadIntegrityError and WriteClashError
284
+ e.object #=> the object which was changed externally
285
+ e.attribute #=> the name of the changed attribute
286
+ e.external_value #=> the new external value
287
+
288
+ e.retry! # Runs the current transaction block again
289
+ e.rollback! # Dismisses all changes in the current section, continues after transaction block
290
+ e.reset! # Resets the whole transaction, continues after transaction block
291
+ e.continue! # Continues with executing the current transaction block
292
+ end
293
+ end
294
+ ```
295
+
296
+ Please note that in most cases calling `rollback!`, `retry!` or `continue!` without any other exception specific method will result in the same error again the next time.
297
+
298
+ ### An attribute we previously read was changed externally
299
+
300
+ A `ReadIntegrityError` is thrown if one transaction reads an attribute value which is then changed externally:
301
+
302
+ ```ruby
303
+ Petra.transaction(identifier: 'tr1') do
304
+ user.last_name = 'the first' if user.first_name = 'Karl'
305
+ end
306
+
307
+ user.first_name = 'Olaf'
308
+
309
+ Petra.transaction(identifier: 'tr1') do
310
+ user.first_name
311
+ #=> Petra::ReadIntegrityError: The attribute `first_name` has been changed externally.
312
+ end
313
+ ```
314
+
315
+ When triggering a `ReadIntegrityError`, you can choose to acknowledge/ignore the external change. Doing so will suppress further errors as long as the external value does not change again.
316
+
317
+ ```ruby
318
+ begin
319
+ ...
320
+ rescue Petra::ReadIntegrityError => e
321
+ e.last_read_value #=> the value we got when last reading the attribute
322
+
323
+ e.ignore!(update_value: true) # we acknowledge the external change and use the new value in our transaction from now on
324
+ e.ignore!(update_value: false) # we keep our old value and simply ignore the external change.
325
+ e.retry!
326
+ end
327
+ ```
328
+
329
+ ### An attribute we changed in our transaction was also changed externally
330
+
331
+ A `WriteClashError` is thrown whenever an attribute we changed inside one of our transaction sections was also changed externally:
332
+
333
+ ```ruby
334
+ Petra.transaction(identifier: 'tr1') do
335
+ user.first_name = 'Foo'
336
+ end
337
+
338
+ user.first_name = 'Moo'
339
+
340
+ Petra.transaction(identifier: 'tr1') do
341
+ user.first_name
342
+ #=> Petra:WriteClashError: The attribute `first_name` has been changed externally and in the transaction.
343
+ end
344
+ ```
345
+
346
+ As both sides changed the attribute value, we have to decided which one to use further in most cases (or completely reset the transaction):
347
+
348
+ ```ruby
349
+ begin
350
+ ...
351
+ rescue Petra::WriteClashError => e
352
+ e.our_value #=> the value we set the attribute to
353
+ e.their_value #=> the new external value
354
+
355
+ e.use_theirs! # undo every change we made to the attribute in this transaction
356
+ e.use_ours! # Ignore the external change, use our value
357
+ e.retry!
358
+ end
359
+ ```
360
+
361
+ ### `continue!`?
362
+
363
+ As mentioned above, `petra` allows the developer to jump back into the transaction after an error was resolved.
364
+ This is done by using Ruby's [Continuation](https://ruby-doc.org/core-2.5.0/Continuation.html) which basically saves a copy of the stack at the time the exception happened. This copy can then be restored if the developer decides to continue the execution.
365
+
366
+ I'd personally keep everything regarding continuations far away from production code, but they are a very interesting concept (which will most likely be removed with Ruby 3.0 :/ ). `examples/continuation_error.rb` shows one of the drawbacks which could lead to a long time of debugging.
367
+
368
+ ```ruby
369
+ begin
370
+ simple_user.first_name = 'Foo'
371
+ simple_user.save
372
+ rescue Petra::WriteClashError => e
373
+ e.use_ours!
374
+ # Jumps back to `simple_user.save` without a retry
375
+ e.continue!
376
+ end
377
+ ```
378
+
379
+ ## Full Configuration Options
380
+
381
+ ### Global Options
382
+
383
+ #### `persistence_adapter`
384
+
385
+ ```ruby
386
+ Petra.configure do
387
+ persistence_adapter :file
388
+ persistence_adapter.storage_directory = '/tmp/petra'
389
+ end
390
+ ```
391
+
392
+ Specifies the persistence adapter and its possible options.
393
+ Petra only includes a file system based adapter by default.
394
+
395
+ #### `instantly_fail_on_read_integrity_errors`
396
+
397
+ ```ruby
398
+ Petra.configure do
399
+ instantly_fail_on_read_integrity_errors false
400
+ end
401
+ ```
402
+
403
+ `petra` can be set to optimistic transaction handling. This means, that a transaction is only checked
404
+ for possible external changes during the commit phase.
405
+
406
+ By default, a corresponding error is thrown directly when the attribute is accessed again within the transaction.
407
+
408
+ #### `log_level`
409
+
410
+ ```ruby
411
+ Petra.configure do
412
+ log_level :debug | :info | :warn | :error
413
+ end
414
+ ```
415
+
416
+ Specifies the log level `petry` should use.
417
+
418
+ * `:debug`
419
+ * Information about all methods called on an object proxy and their results
420
+ * Attribute reads and changes
421
+ * Acquired and released locks
422
+ * The creation of transaction log entries
423
+ * `:info`
424
+ * Starting and persisting a transaction
425
+ * Committing a transaction
426
+ * Triggering a rollback on a transaction
427
+ * `:warn`
428
+ * Forced transaction resets
429
+
430
+ ### Class Specific Options
431
+
432
+ Apart from the already mentioned ones, the following class specific options are available:
433
+
434
+ #### `proxy_instances`
435
+
436
+ Determines whether `petra` should automatically create proxies for instances of the configured class when they are accessed from within an existing object proxy.
437
+
438
+ ```ruby
439
+ Petra.configure do
440
+ configure_class SimpleUser do
441
+ proxy_instances true
442
+ end
443
+
444
+ # Do not create a proxy for strings. Otherwise, calling `SimpleUser#first_name` would result in a string object proxy
445
+ configure_class String do
446
+ proxy_instances false
447
+ end
448
+ end
449
+ ```
450
+
451
+ #### `use_specialized_proxy`
452
+
453
+ `petra` contains a very basic `ObjectProxy` implementation which works fine with most ruby objects, but has to be configured.
454
+ For more advanced classes, it is advised to create a specialized proxy (see `petra-rails`).
455
+
456
+ By default, `petra` will use the specialized version if available, but can be forced to use the basic object proxy instead:
457
+
458
+ ```ruby
459
+ Petra.configure do
460
+ configure_class ActiveRecord::Base do
461
+ use_specialized_proxy false
462
+ end
463
+ end
464
+ ```
465
+
466
+ #### `mixin_module_proxies`
467
+
468
+ `petra` does not only support proxies for certain classes, but also for mixins. This allows a developer to define a proxy which is automatically used for every class which contains a certain module.
469
+
470
+ By default, `petra` contains an `Enumerable` proxy which automatically wraps its entries in object proxies.
471
+
472
+ The automatic inclusion of these module proxies can be disabled:
473
+
474
+ ```ruby
475
+ Petra.configure do
476
+ configure_class Array do
477
+ mixin_module_proxies false
478
+ end
479
+ end
480
+ ```
481
+
482
+ #### `id_method`
483
+
484
+ Specifies the method to retrieve an identifier for instances of the configured class.
485
+
486
+ By default, `object_id` is used, which of course is very limited.
487
+
488
+ ```ruby
489
+ Petra.configure do
490
+ configure_class ActiveRecord::Base do
491
+ id_method :id
492
+ # or
493
+ id_method do |obj|
494
+ obj.id
495
+ end
496
+ end
497
+ end
498
+ ```
499
+
500
+ #### `lookup_method`
501
+
502
+ Basically the counterpart of `id_method`. Specifies the class method which can be used to retrieve an instance of the configured class when providing the corresponding identifier.
503
+
504
+ It defaults to `ObjectSpace._id2ref` which returns an object by its `object_id`.
505
+
506
+ ```ruby
507
+ Petra.configure do
508
+ configure_class ActiveRecord::Base do
509
+ lookup_method :find
510
+ end
511
+ end
512
+ ```
513
+
514
+ #### `init_method`
515
+
516
+ Specifies the method to initialize a new instance of the configured class (or one of its descendants).
517
+ It is used to automatically re-initialize objects used (and persisted) in a previous section and works the same way as lookup_method.
518
+
519
+ ```ruby
520
+ Petra.configure do
521
+ configure_class Array do
522
+ init_method :new
523
+ end
524
+ end
525
+ ```
526
+
527
+ ## Extending `petra`
528
+
529
+ `petra` can be easily extended to a certain extent as seen in [stex/petra-rails](https://github.com/stex/petra-rails).
530
+
531
+ ### Class Proxies
532
+
533
+ As mentioned above, some classes are too complicated to be configured using the basic `ObjectProxy`.
534
+
535
+ Let's define a basic example for such a class:
536
+
537
+ ```ruby
538
+ class SimpleRecord
539
+ def self.create(attributes = {})
540
+ new(attributes).save
541
+ end
542
+
543
+ def save
544
+ # some persistence logic
545
+ end
546
+ end
547
+ ```
548
+
549
+ In this example, `#create` is a method we cannot configure easily as it doesn't match any of the available method types in `ObjectProxy`. Instead. it is a combination of attribute writers and persistence methods.
550
+
551
+ To be taken into account as a custom object proxy, a class has to comply to the following rules:
552
+
553
+ 1. It has to be defined inside `Petra::Proxies`
554
+ 2. It has to inherit from `Petra::Proxies::ObjectProxy`
555
+ 3. It has to define the class names it may be applied to in a constant named `CLASS_NAMES`
556
+
557
+ Let's define the corresponding proxy for `SimpleRecord`:
558
+
559
+ ```ruby
560
+ module Petra
561
+ module Proxies
562
+ class SimpleRecordProxy < ObjectProxy
563
+ CLASS_NAMES = %w[SimpleRecord].freeze
564
+
565
+ def create(attributes = {})
566
+ # This method may only be called on class, not on instance level
567
+ class_method!
568
+
569
+ # Use ObjectProxy's basic `new` method without any arguments
570
+ new.tap do |obj|
571
+ # Tell our transaction that we initialized a new object.
572
+ # This wasn't done in the previous examples as we were working on the
573
+ # `ObjectSpace` with objects defined outside the transaction.
574
+ transaction.log_object_initialization(o, method: 'new')
575
+
576
+ # Apply the attribute writes inside the transaction
577
+ attributes.each do |k, v|
578
+ __set_attribute(k, v)
579
+ end
580
+
581
+ # #create automatically persists a record, we therefore have to
582
+ # tell our transaction to log this action.
583
+ transaction.log_object_persistence(o, method: 'save')
584
+ end
585
+ end
586
+
587
+ def save
588
+ transaction.log_object_persistence(self, method: 'save')
589
+ end
590
+ end
591
+ end
592
+ end
593
+ ```
594
+
595
+ See [petra-rails's ActiveRecordProxy](https://github.com/Stex/petra-rails/blob/master/lib/petra/proxies/active_record_proxy.rb) for a full example.
596
+
597
+ ### Module Proxies
598
+
599
+ As mentioned above, module proxies can be used to define proxy functionality for all classes which include a certain module.
600
+ Internally, these modules are included into the singleton class of our object proxies, meaning that one instance of a proxy could include a certain module, the other doesn't.
601
+
602
+ A module proxy has to comply to the following rules:
603
+
604
+ 1. It has to be defined in `Petra::Proxies`
605
+ 2. It has to include `Petra::Proxies::ModuleProxy`
606
+ 3. It has to define a constant named `MODULE_NAMES` which contains the modules it is applicable for.
607
+
608
+ Let's take a look at `petra`'s `EnumerableProxy`:
609
+
610
+ ```ruby
611
+ module Petra
612
+ module Proxies
613
+ module EnumerableProxy
614
+ include ModuleProxy
615
+ MODULE_NAMES = %w[Enumerable].freeze
616
+
617
+ # Specifying an `INCLUDES` constant leads to instances of the resulting proxy
618
+ # automatically including the given modules - in this case, every proxy which handles
619
+ # an Enumerable will automatically be an Enumerable as well
620
+ INCLUDES = [Enumerable].freeze
621
+
622
+ # ModuleProxies may specify an `InstanceMethods` and a `ClassMethods` sub-module.
623
+ # Their methods will be included/extended accordingly.
624
+ module InstanceMethods
625
+ #
626
+ # We have to define our own #each method for the singleton class' Enumerable
627
+ # It basically just wraps the original enum's entries in proxies and executes
628
+ # the "normal" #each
629
+ #
630
+ def each(&block)
631
+ Petra::Proxies::EnumerableProxy.proxy_entries(proxied_object).each(&block)
632
+ end
633
+ end
634
+
635
+ #
636
+ # Ensures the the objects yielded to blocks are actually petra proxies.
637
+ # This is necessary as the internal call to +each+ would be forwarded to the
638
+ # actual Enumerable object and result in unproxied objects.
639
+ #
640
+ # This method will only proxy objects which allow this through the class config
641
+ # as the enum's entries are seen as inherited objects.
642
+ # `[]` is used as method causing the proxy creation as it's closest to what's actually happening.
643
+ #
644
+ # @return [Array<Petra::Proxies::ObjectProxy>]
645
+ #
646
+ def self.proxy_entries(enum, surrogate_method: '[]')
647
+ enum.entries.map { |o| o.petra(inherited: true, configuration_args: [surrogate_method]) }
648
+ end
649
+ end
650
+ end
651
+ end
652
+ ```
653
+
654
+ Please take a look at [`lib/petra/proxies/abstract_proxy.rb`](https://github.com/Stex/petra/blob/master/lib/petra/proxies/abstract_proxy.rb) for more information regarding how proxies are chosen and built.
655
+
656
+ ### Persistence Adapters
657
+
658
+ For its transaction handling, `petra` needs access to a storage with atomic write operations to store its transaction logs as well as being able to lock certain resources (during commit phase, no other transaction may have access to certain resources).
659
+
660
+ [`Petra::PersistenceAdapters::Adapter`](https://github.com/Stex/petra/blob/master/lib/petra/persistence_adapters/adapter.rb) provides an interface for classes which provide this functionality. [`FileAdapter`](https://github.com/Stex/petra/blob/master/lib/petra/persistence_adapters/file_adapter.rb) is the reference implementation which uses the file system and UNIX file locks.
661
+
662
+ #### Required Methods
663
+
664
+ **`persist!`**
665
+
666
+ Saves all available transaction log entries to the storage.
667
+ Log entries are added using `#enqueue(entry)` and available as `queue` inside your adapter instance.
668
+
669
+ * A transaction lock has to be applied
670
+ * Entries have to be marked as persisted afterwards using `entry.mark_as_persisted!`
671
+
672
+
673
+ **`transaction_identifiers`**
674
+
675
+ Should return the identifiers of all transactions which were started, but not yet committed.
676
+
677
+ **`savepoints(transaction)`**
678
+
679
+ Should return all savepoints (section identifiers) for the given transaction,
680
+
681
+ **`log_entries(section)`**
682
+
683
+ Should return all log entries which were persisted for the given section in the past.
684
+
685
+ **`reset_transaction(transaction)`**
686
+
687
+ Removes all information currently stored regarding the given transaction
688
+
689
+ **`with_global_lock(suspend:, &block)`**
690
+
691
+ Acquires a global lock (only one thread may hold it at the same time), runs the given block and releases the global lock again.
692
+
693
+ If `suspend` is set to `true`, the execution will wait for the lock to be available, otherwise, a `Petra::LockError` is thrown if the lock is not available.
694
+
695
+ You have to make sure that the lock is freed again if an error occurs within the given block or your own implementation.
696
+
697
+ **`with_transaction_lock(transaction, suspend:)`**
698
+
699
+ Acquires a lock on the given transaction.
700
+
701
+ **`with_object_lock(object, suspend:)`**
702
+
703
+ Acquires a lock on the given Object (Proxy).
704
+
705
+ Make sure that your implementation allows one thread locking the resource multiple times without stalling.
706
+
707
+ ```ruby
708
+ with_object_lock(obj1) do
709
+ with_object_lock(obj1) do # Should work as we already hold the lock
710
+ ...
711
+ end
712
+ end
713
+ ```
714
+
715
+
716
+ #### Registering a new adapter
717
+
718
+ Similar to Rails' mailer adapters, new adapter can be registered under a given name and be used in `petra`'s configuration afterwards:
719
+
720
+ ```ruby
721
+ Petra::PersistenceAdapters::Adapter.register_adapter(:redis, RedisAdapter)
722
+
723
+ Petra.configure do
724
+ persistence_adapter :redis
725
+ end
726
+ ```