petra_core 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/.rspec +3 -0
- data/.rubocop.yml +83 -0
- data/.ruby-version +1 -0
- data/.travis.yml +5 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +13 -0
- data/Gemfile.lock +74 -0
- data/MIT-LICENSE +20 -0
- data/README.md +726 -0
- data/Rakefile +8 -0
- data/bin/console +8 -0
- data/bin/setup +8 -0
- data/examples/continuation_error.rb +125 -0
- data/examples/dining_philosophers.rb +138 -0
- data/examples/showcase.rb +54 -0
- data/lib/petra/components/entries/attribute_change.rb +29 -0
- data/lib/petra/components/entries/attribute_change_veto.rb +37 -0
- data/lib/petra/components/entries/attribute_read.rb +20 -0
- data/lib/petra/components/entries/object_destruction.rb +22 -0
- data/lib/petra/components/entries/object_initialization.rb +19 -0
- data/lib/petra/components/entries/object_persistence.rb +26 -0
- data/lib/petra/components/entries/read_integrity_override.rb +42 -0
- data/lib/petra/components/entry_set.rb +87 -0
- data/lib/petra/components/log_entry.rb +342 -0
- data/lib/petra/components/proxy_cache.rb +209 -0
- data/lib/petra/components/section.rb +543 -0
- data/lib/petra/components/transaction.rb +405 -0
- data/lib/petra/components/transaction_manager.rb +214 -0
- data/lib/petra/configuration/base.rb +132 -0
- data/lib/petra/configuration/class_configurator.rb +309 -0
- data/lib/petra/configuration/configurator.rb +67 -0
- data/lib/petra/core_ext.rb +27 -0
- data/lib/petra/exceptions.rb +181 -0
- data/lib/petra/persistence_adapters/adapter.rb +154 -0
- data/lib/petra/persistence_adapters/file_adapter.rb +239 -0
- data/lib/petra/proxies/abstract_proxy.rb +149 -0
- data/lib/petra/proxies/enumerable_proxy.rb +44 -0
- data/lib/petra/proxies/handlers/attribute_read_handler.rb +45 -0
- data/lib/petra/proxies/handlers/missing_method_handler.rb +47 -0
- data/lib/petra/proxies/method_handlers.rb +213 -0
- data/lib/petra/proxies/module_proxy.rb +12 -0
- data/lib/petra/proxies/object_proxy.rb +310 -0
- data/lib/petra/util/debug.rb +45 -0
- data/lib/petra/util/extended_attribute_accessors.rb +51 -0
- data/lib/petra/util/field_accessors.rb +35 -0
- data/lib/petra/util/registrable.rb +48 -0
- data/lib/petra/util/test_helpers.rb +9 -0
- data/lib/petra/version.rb +5 -0
- data/lib/petra.rb +100 -0
- data/lib/tasks/petra_tasks.rake +5 -0
- data/petra.gemspec +36 -0
- 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
|
+
```
|