trx_ext 1.0.0
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.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +7 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/Gemfile +12 -0
- data/LICENSE.txt +21 -0
- data/README.md +622 -0
- data/Rakefile +4 -0
- data/bin/console +29 -0
- data/bin/setup +8 -0
- data/bin/test_all_ar_versions +11 -0
- data/lib/trx_ext/callback_pool.rb +108 -0
- data/lib/trx_ext/config.rb +12 -0
- data/lib/trx_ext/object_ext.rb +42 -0
- data/lib/trx_ext/railtie.rb +9 -0
- data/lib/trx_ext/retry.rb +64 -0
- data/lib/trx_ext/transaction.rb +34 -0
- data/lib/trx_ext/version.rb +6 -0
- data/lib/trx_ext.rb +55 -0
- data/log/.gitkeep +0 -0
- data/trx_ext.gemspec +41 -0
- metadata +167 -0
data/README.md
ADDED
@@ -0,0 +1,622 @@
|
|
1
|
+
# TrxExt
|
2
|
+
|
3
|
+
Extends functionality of ActiveRecord's transaction to auto-retry failed SQL transaction in case of deadlock, serialization error or unique constraint error. It also allows you to define `on_complete` callback that is being executed after SQL transaction is finished(either COMMIT-ed or ROLLBACK-ed).
|
4
|
+
|
5
|
+
Currently, the implementation only works for ActiveRecord PostgreSQL adapter. Feel free to improve it.
|
6
|
+
|
7
|
+
**WARNING!**
|
8
|
+
|
9
|
+
Because the implementation of this gem is a patch for `ActiveRecord::ConnectionAdapters::PostgreSQLAdapter` - carefully test its integration into your project. For example, if your project patches ActiveRecord or if some of your gems patches ActiveRecord - there might be conflicts in the implementation which could potentially lead to the data loss.
|
10
|
+
|
11
|
+
Currently, the implementation is tested for `6.0.4.1` and `6.1.4.1` versions of ActiveRecord(see [TrxExt::SUPPORTED_AR_VERSIONS](lib/trx_ext/version.rb))
|
12
|
+
|
13
|
+
## Requirements
|
14
|
+
|
15
|
+
- ActiveRecord 6+
|
16
|
+
- Ruby 3
|
17
|
+
- PostgreSQL 9.1+
|
18
|
+
|
19
|
+
## Installation
|
20
|
+
|
21
|
+
Add this line to your application's Gemfile:
|
22
|
+
|
23
|
+
```ruby
|
24
|
+
gem 'trx_ext'
|
25
|
+
```
|
26
|
+
|
27
|
+
And then execute:
|
28
|
+
|
29
|
+
$ bundle install
|
30
|
+
|
31
|
+
Or install it yourself as:
|
32
|
+
|
33
|
+
$ gem install trx_ext
|
34
|
+
|
35
|
+
## Usage
|
36
|
+
|
37
|
+
```ruby
|
38
|
+
require 'trx_ext'
|
39
|
+
require 'active_record'
|
40
|
+
|
41
|
+
# Object #trx is a shorthand of ActiveRecord::Base.transaction
|
42
|
+
trx do
|
43
|
+
DummyRecord.first || DummyRecord.create
|
44
|
+
end
|
45
|
+
|
46
|
+
trx do
|
47
|
+
DummyRecord.first || DummyRecord.create
|
48
|
+
trx do |c|
|
49
|
+
c.on_complete { puts "This message will be printed after COMMIT statement." }
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
trx do
|
54
|
+
DummyRecord.first || DummyRecord.create
|
55
|
+
trx do |c|
|
56
|
+
c.on_complete { puts "This message will be printed after ROLLBACK statement." }
|
57
|
+
end
|
58
|
+
raise ActiveRecord::Rollback
|
59
|
+
end
|
60
|
+
|
61
|
+
class DummyRecord
|
62
|
+
# Wrap method in transaction
|
63
|
+
wrap_in_trx def some_method_with_quieries
|
64
|
+
DummyRecord.first || DummyRecord.create
|
65
|
+
end
|
66
|
+
end
|
67
|
+
```
|
68
|
+
|
69
|
+
## Configuration
|
70
|
+
|
71
|
+
```ruby
|
72
|
+
TrxExt.configure do |c|
|
73
|
+
# Number of retries before failing when unique constraint error raises. Default is 5
|
74
|
+
c.unique_retries = 5
|
75
|
+
end
|
76
|
+
```
|
77
|
+
|
78
|
+
## How it works?
|
79
|
+
|
80
|
+
Your either every single AR SQL query or whole AR transaction is retried whenever it throws deadlock error, serialization error or unique constraint error. In case of AR transaction - the block of code that the AR transaction belongs to is re-executed, thus the transaction is retried.
|
81
|
+
|
82
|
+
## Rules you have to stick when using this gem
|
83
|
+
|
84
|
+
> "Don't put more into a single transaction than needed for integrity purposes."
|
85
|
+
>
|
86
|
+
> — [PostgreSQL documentation]
|
87
|
+
|
88
|
+
Since `ActiveRecord::ConnectionAdapters::PostgreSQLAdapter` is now patched with `TrxExt::Retry.with_retry_until_serialized`, there's no need to wrap every AR query in a `trx` block to ensure integrity. Wrap code in an explicit `trx` block if and only if it can or does produce two or more SQL queries *and* it is important to run those queries together atomically.
|
89
|
+
|
90
|
+
There is "On complete" feature that allows you to define callbacks(blocks of code) that will be executed after transaction is complete. See `On complete callbacks` section bellow for the docs. See `On complete callbacks integrity` section bellow to be aware about different situations with them.
|
91
|
+
|
92
|
+
* Don't explicitly wrap queries.
|
93
|
+
|
94
|
+
#### Bad
|
95
|
+
|
96
|
+
```ruby
|
97
|
+
trx { User.find_by(username: 'someusername') }
|
98
|
+
```
|
99
|
+
|
100
|
+
#### Good
|
101
|
+
|
102
|
+
```ruby
|
103
|
+
User.find_by(username: 'someusername')
|
104
|
+
```
|
105
|
+
|
106
|
+
* Don't wrap multiple `SELECT` queries in a single transaction unless it is of vital importance (see epigraph).
|
107
|
+
|
108
|
+
#### Bad
|
109
|
+
|
110
|
+
```ruby
|
111
|
+
trx do
|
112
|
+
@author = User.first
|
113
|
+
@posts = current_user.posts.load
|
114
|
+
end
|
115
|
+
```
|
116
|
+
|
117
|
+
```sql
|
118
|
+
BEGIN
|
119
|
+
SELECT "users".* FROM "users" ...
|
120
|
+
SELECT "posts".* FROM "posts" ...
|
121
|
+
COMMIT
|
122
|
+
```
|
123
|
+
|
124
|
+
#### Good
|
125
|
+
|
126
|
+
```ruby
|
127
|
+
@author = User.first
|
128
|
+
@posts = current_user.posts.load
|
129
|
+
```
|
130
|
+
|
131
|
+
```sql
|
132
|
+
-- TrxExt::Retry.with_retry_until_serialized {
|
133
|
+
SELECT "users".* FROM "users" ...
|
134
|
+
-- }
|
135
|
+
-- TrxExt::Retry.with_retry_until_serialized {
|
136
|
+
SELECT "posts".* FROM "posts" ...
|
137
|
+
-- }
|
138
|
+
```
|
139
|
+
|
140
|
+
* Beware of `AR::Relation` lazy loading if it is necessary to have multiple `SELECT`s in a single transaction.
|
141
|
+
|
142
|
+
#### Bad
|
143
|
+
|
144
|
+
```ruby
|
145
|
+
trx do
|
146
|
+
@posts = Post.all
|
147
|
+
@users = User.all
|
148
|
+
end
|
149
|
+
```
|
150
|
+
|
151
|
+
will result in no query.
|
152
|
+
|
153
|
+
#### Good
|
154
|
+
|
155
|
+
```ruby
|
156
|
+
trx do
|
157
|
+
@posts = Post.all
|
158
|
+
@users = User.all
|
159
|
+
end
|
160
|
+
```
|
161
|
+
|
162
|
+
```sql
|
163
|
+
BEGIN
|
164
|
+
SELECT "posts".* FROM "posts" ...
|
165
|
+
SELECT "users".* FROM "users" ...
|
166
|
+
COMMIT
|
167
|
+
```
|
168
|
+
|
169
|
+
* When performing `UPDATE`/`INSERT` queries that depend on record's state - reload that record in the beginning of `trx` block.
|
170
|
+
|
171
|
+
#### Bad
|
172
|
+
```ruby
|
173
|
+
def initialize(user)
|
174
|
+
@user = user
|
175
|
+
end
|
176
|
+
|
177
|
+
def update_posts
|
178
|
+
trx do
|
179
|
+
@user.posts.update_all(banned: true) if @user.user_permission.admin?
|
180
|
+
end
|
181
|
+
end
|
182
|
+
```
|
183
|
+
|
184
|
+
```sql
|
185
|
+
BEGIN
|
186
|
+
UPDATE posts SET banned = TRUE WHERE posts.user_id IN (...)
|
187
|
+
COMMIT
|
188
|
+
```
|
189
|
+
|
190
|
+
#### Explanation
|
191
|
+
It might not be obvious that this code depends on `@user` - `UserPermission#admin?` is used to detect whether `Post#banned` must be updated. However, it is accessed through `@user` and there is no guarantee that, when calling `@user.user_permission`, it was not already cached by either previous calls, upper by stack trace, or inside `trx` block on transaction retry. This is why it is mandatory to call `@user.reload` - to reset user's cache and the cache of user's relations.
|
192
|
+
|
193
|
+
#### Good
|
194
|
+
```ruby
|
195
|
+
def initialize(user)
|
196
|
+
@user = user
|
197
|
+
end
|
198
|
+
|
199
|
+
def update_posts
|
200
|
+
trx do
|
201
|
+
@user.reload
|
202
|
+
@user.posts.update_all(banned: true) if @user.user_permission.admin?
|
203
|
+
end
|
204
|
+
end
|
205
|
+
```
|
206
|
+
|
207
|
+
```sql
|
208
|
+
BEGIN
|
209
|
+
SELECT * FROM users WHERE users.id = ...
|
210
|
+
SELECT * FROM user_permissions WHERE user_permissions.user_id = ...
|
211
|
+
UPDATE posts SET banned = TRUE WHERE posts.id IN (...)
|
212
|
+
COMMIT
|
213
|
+
```
|
214
|
+
|
215
|
+
* It may happen that you need to invoke mailer's method inside `trx` block and pass there values that are calculated within the transaction block. Normally, you need to extract those values into after-transaction code and invoke mailer after transaction's end. Use `on_complete` callback to simplify your code:
|
216
|
+
|
217
|
+
#### Bad
|
218
|
+
```ruby
|
219
|
+
trx do
|
220
|
+
user = User.find_or_initialize_by(email: email)
|
221
|
+
if user.save
|
222
|
+
# May be invoked more than one time if transaction is retried
|
223
|
+
Mailer.registration_confirmation(user.id).deliver_later
|
224
|
+
end
|
225
|
+
end
|
226
|
+
```
|
227
|
+
|
228
|
+
#### Good (before refactoring)
|
229
|
+
```ruby
|
230
|
+
user = nil
|
231
|
+
result =
|
232
|
+
trx do
|
233
|
+
user = User.find_or_initialize_by(email: email)
|
234
|
+
user.save
|
235
|
+
end
|
236
|
+
Mailer.registration_confirmation(user.id).deliver_later if result
|
237
|
+
```
|
238
|
+
|
239
|
+
#### Good (after refactoring)
|
240
|
+
```ruby
|
241
|
+
trx do |c|
|
242
|
+
user = User.find_or_initialize_by(email: email)
|
243
|
+
if user.save
|
244
|
+
c.on_complete { Mailer.registration_confirmation(user.id).deliver_later }
|
245
|
+
end
|
246
|
+
end
|
247
|
+
```
|
248
|
+
|
249
|
+
* Always keep in mind, that retrying of transactions is just re-execution of ruby's block of code on transaction retry. If you have any variables, that are changing inside the block - ensure that their values are reset in the beginning of block. Don't use methods that will raise error if called more than twice.
|
250
|
+
|
251
|
+
#### Bad
|
252
|
+
```ruby
|
253
|
+
resurrected_users_count = 0
|
254
|
+
trx do
|
255
|
+
User.deleted.find_each do |user|
|
256
|
+
if user.created_at > 2.days.ago
|
257
|
+
user.active!
|
258
|
+
resurrected_users_count += 1
|
259
|
+
end
|
260
|
+
end
|
261
|
+
end
|
262
|
+
puts resurrected_users_count
|
263
|
+
```
|
264
|
+
|
265
|
+
#### Good
|
266
|
+
```ruby
|
267
|
+
resurrected_users_count = nil
|
268
|
+
trx do
|
269
|
+
resurrected_users_count = 0
|
270
|
+
User.deleted.find_each do |user|
|
271
|
+
if user.created_at > 2.days.ago
|
272
|
+
user.active!
|
273
|
+
resurrected_users_count += 1
|
274
|
+
end
|
275
|
+
end
|
276
|
+
end
|
277
|
+
puts resurrected_users_count
|
278
|
+
```
|
279
|
+
|
280
|
+
#### Bad
|
281
|
+
```ruby
|
282
|
+
class UsersController
|
283
|
+
def update
|
284
|
+
# This may raise AbstractController::DoubleRenderError if either redirect or render invoked twice
|
285
|
+
trx do
|
286
|
+
if @user.update(user_params)
|
287
|
+
redirect_to @user
|
288
|
+
else
|
289
|
+
render :edit
|
290
|
+
end
|
291
|
+
end
|
292
|
+
end
|
293
|
+
end
|
294
|
+
```
|
295
|
+
|
296
|
+
#### Bad
|
297
|
+
```ruby
|
298
|
+
class UsersController
|
299
|
+
# This may raise AbstractController::DoubleRenderError if either redirect or render invoked twice
|
300
|
+
wrap_in_trx def update
|
301
|
+
if @user.update(user_params)
|
302
|
+
redirect_to @user
|
303
|
+
else
|
304
|
+
render :edit
|
305
|
+
end
|
306
|
+
end
|
307
|
+
end
|
308
|
+
```
|
309
|
+
|
310
|
+
#### Good
|
311
|
+
```ruby
|
312
|
+
class UsersController
|
313
|
+
def update
|
314
|
+
if @user.update(user_params)
|
315
|
+
redirect_to @user
|
316
|
+
else
|
317
|
+
render :edit
|
318
|
+
end
|
319
|
+
end
|
320
|
+
end
|
321
|
+
```
|
322
|
+
|
323
|
+
#### Good
|
324
|
+
```ruby
|
325
|
+
class UsersController
|
326
|
+
def update
|
327
|
+
trx do |c|
|
328
|
+
if @user.update(user_params)
|
329
|
+
c.on_complete { redirect_to @user }
|
330
|
+
else
|
331
|
+
c.on_complete { render :edit }
|
332
|
+
end
|
333
|
+
end
|
334
|
+
end
|
335
|
+
end
|
336
|
+
```
|
337
|
+
|
338
|
+
* Carefully implement the code that is related to the non-relational databases like Redis or MongoDB
|
339
|
+
|
340
|
+
#### Bad
|
341
|
+
```ruby
|
342
|
+
trx do
|
343
|
+
@post.reload
|
344
|
+
if @post.tags_arr.include?('special')
|
345
|
+
@post.update_columns(special: true)
|
346
|
+
@post.mongo_post.update(special: true)
|
347
|
+
end
|
348
|
+
end
|
349
|
+
```
|
350
|
+
|
351
|
+
#### Explanation
|
352
|
+
|
353
|
+
Example: `@post.tags_arr.include?('special') == true` and, as a result, `@post.mongo_post.update(special: true)` is executed but transaction is failed to be serialized. On second try - `@post.tags_arr.include?('special')` becomes false but the value of `MongoPost#special` was already changed
|
354
|
+
|
355
|
+
#### Good
|
356
|
+
```ruby
|
357
|
+
trx do
|
358
|
+
@post.reload
|
359
|
+
if @post.tags_arr.include?('special')
|
360
|
+
@post.update_columns(special: true)
|
361
|
+
end
|
362
|
+
@post.mongo_post.update(special: @post.tags_arr.include?('special'))
|
363
|
+
end
|
364
|
+
```
|
365
|
+
|
366
|
+
* Don't explicitly use `return` in the transaction's block of code. It may affect on how the transaction is going to be finished. Currently, it finishes with `COMPLETE` statement, but in the future versions it may change - according to the [warning message](https://github.com/rails/rails/blob/v6.1.3.2/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb#L330-L337), the behaviour may change soon.
|
367
|
+
|
368
|
+
#### Bad
|
369
|
+
```ruby
|
370
|
+
def some_method
|
371
|
+
trx do
|
372
|
+
return if User.where(email: email).exists?
|
373
|
+
|
374
|
+
User.create(email: email)
|
375
|
+
end
|
376
|
+
end
|
377
|
+
```
|
378
|
+
|
379
|
+
#### Bad
|
380
|
+
```ruby
|
381
|
+
def some_method
|
382
|
+
trx do |c|
|
383
|
+
user = User.find_by(email: email)
|
384
|
+
return user if user
|
385
|
+
|
386
|
+
user = User.create(email: email)
|
387
|
+
c.on_complete { Mailer.registration_confirmation(user.id).deliver_later }
|
388
|
+
end
|
389
|
+
end
|
390
|
+
```
|
391
|
+
|
392
|
+
#### Explanation
|
393
|
+
Using `return` in the `Proc`(a block of code is a `Proc`) will return from the stack call instead the return from the block of code. Example:
|
394
|
+
|
395
|
+
```
|
396
|
+
def some_method
|
397
|
+
puts "Start"
|
398
|
+
yield
|
399
|
+
puts "End"
|
400
|
+
end
|
401
|
+
|
402
|
+
def another_method
|
403
|
+
some_method do
|
404
|
+
puts "Hi"
|
405
|
+
return
|
406
|
+
end
|
407
|
+
end
|
408
|
+
```
|
409
|
+
Calling `#another_method` will output `Start` and `Hi` string, `End` string will never get output. Refer to [official docs](https://ruby-doc.org/core-3.0.1/Proc.html#class-Proc-label-Lambda+and+non-lambda+semantics) for more info.
|
410
|
+
|
411
|
+
#### Good
|
412
|
+
```ruby
|
413
|
+
def some_method
|
414
|
+
trx do
|
415
|
+
unless User.where(email: email).exists?
|
416
|
+
User.create(email: email)
|
417
|
+
end
|
418
|
+
end
|
419
|
+
end
|
420
|
+
```
|
421
|
+
|
422
|
+
#### Good
|
423
|
+
```ruby
|
424
|
+
wrap_in_trx def some_method
|
425
|
+
return if User.where(email: email).exists?
|
426
|
+
|
427
|
+
User.create(email: email)
|
428
|
+
end
|
429
|
+
```
|
430
|
+
|
431
|
+
#### Good
|
432
|
+
```ruby
|
433
|
+
wrap_in_trx def some_method
|
434
|
+
user = User.find_by(email: email)
|
435
|
+
return user if user
|
436
|
+
|
437
|
+
user = User.create(email: email)
|
438
|
+
trx { |c| c.on_complete { Mailer.registration_confirmation(user.id).deliver_later } }
|
439
|
+
end
|
440
|
+
```
|
441
|
+
|
442
|
+
## On complete callbacks
|
443
|
+
|
444
|
+
On-complete callbacks are defined with `TrxExt::CallbackPool#on_complete` method. An instance of `TrxExt::CallbackPool` is passed in each transaction block. You may add as much on-complete callbacks as you want by calling `TrxExt::CallbackPool#on_complete` several times - they will be executed in the order you define
|
445
|
+
them(FIFO principle). The on-complete callbacks from nested transactions will be executed from the most deep to the most top transaction. Another words, if top transaction defines `<#TrxExt::CallbackPool 0x1>` instance and nested transaction defines `<#TrxExt::CallbackPool 0x2>` instance then, when executing on-complete callbacks - the callbacks of `<#TrxExt::CallbackPool 0x2>` instance will be executed first(FILO principle).
|
446
|
+
|
447
|
+
Example:
|
448
|
+
|
449
|
+
```ruby
|
450
|
+
ActiveRecord::Base.transaction do |c1|
|
451
|
+
User.first
|
452
|
+
c1.on_complete { puts "This is 3rd message" }
|
453
|
+
ActiveRecord::Base.transaction do |c2|
|
454
|
+
User.last
|
455
|
+
c2.on_complete { puts "This is 2nd message" }
|
456
|
+
ActiveRecord::Base.transaction do |c3|
|
457
|
+
c3.on_complete { puts "This is 1st message" }
|
458
|
+
User.first(2)
|
459
|
+
end
|
460
|
+
end
|
461
|
+
c1.on_complete { puts "This is 4th message" }
|
462
|
+
end
|
463
|
+
```
|
464
|
+
|
465
|
+
If you don't need to define on-complete callbacks - you may skip explicit definition of block's argument.
|
466
|
+
|
467
|
+
Example:
|
468
|
+
|
469
|
+
```ruby
|
470
|
+
ActiveRecord::Base.transaction { User.first }
|
471
|
+
```
|
472
|
+
|
473
|
+
Keep in mind, that all on-complete callbacks are not a part of the transaction. If you want to make it transactional - you need to wrap it in another transaction.
|
474
|
+
|
475
|
+
Example:
|
476
|
+
|
477
|
+
```ruby
|
478
|
+
ActiveRecord::Base.transaction do |c1|
|
479
|
+
User.first
|
480
|
+
c1.on_complete do
|
481
|
+
ActiveRecord::Base.transaction do
|
482
|
+
User.find_or_create_by(email: email)
|
483
|
+
end
|
484
|
+
end
|
485
|
+
end
|
486
|
+
```
|
487
|
+
|
488
|
+
You may define on-complete callbacks inside another on-complete callbacks. You may define another transactions in
|
489
|
+
on-complete callbacks. Just don't get confused in the order they are going to be executed.
|
490
|
+
|
491
|
+
Example:
|
492
|
+
|
493
|
+
```ruby
|
494
|
+
ActiveRecord::Base.transaction do |c1|
|
495
|
+
User.first
|
496
|
+
c1.on_complete do
|
497
|
+
puts "This line will be executed first"
|
498
|
+
ActiveRecord::Base.transaction do |c2|
|
499
|
+
User.last
|
500
|
+
c2.on_complete do
|
501
|
+
puts "This line will be executed second"
|
502
|
+
end
|
503
|
+
end
|
504
|
+
puts "This line will be executed third"
|
505
|
+
end
|
506
|
+
end
|
507
|
+
```
|
508
|
+
|
509
|
+
Also, please avoid usage of the callbacks that belong to one transaction in another transaction explicitly. This complicates the readability of the code.
|
510
|
+
|
511
|
+
Example:
|
512
|
+
|
513
|
+
```ruby
|
514
|
+
ActiveRecord::Base.transaction do |c1|
|
515
|
+
User.first
|
516
|
+
c1.on_complete do
|
517
|
+
ActiveRecord::Base.transaction do
|
518
|
+
User.last
|
519
|
+
c1.on_complete do
|
520
|
+
puts "This will be executed at the time when parent transaction's on-complete callbacks are executed!"
|
521
|
+
end
|
522
|
+
end
|
523
|
+
end
|
524
|
+
end
|
525
|
+
```
|
526
|
+
|
527
|
+
## On complete callbacks integrity
|
528
|
+
|
529
|
+
* Don't define callbacks blocks as lambdas unless you are 100% sure what you are doing. Lambda has a bit different behaviour comparing to Proc. Refer to [ruby documentation](https://ruby-doc.org/core-2.6.5/Proc.html#class-Proc-label-Lambda+and+non-lambda+semantics).
|
530
|
+
|
531
|
+
* When defining a callback - make sure that it does not depend on transaction's integrity. Another words - define it in a way like it is a normal code implementation outside the transaction:
|
532
|
+
|
533
|
+
#### Bad
|
534
|
+
```ruby
|
535
|
+
trx do |c|
|
536
|
+
user = User.find(id)
|
537
|
+
user.referrals.create(referral_attrs)
|
538
|
+
c.on_complete do
|
539
|
+
Mailer.new_referral(
|
540
|
+
user_id: user.id, total_referrals: user.referrals.count
|
541
|
+
).deliver_later
|
542
|
+
end
|
543
|
+
end
|
544
|
+
```
|
545
|
+
|
546
|
+
#### Explanation
|
547
|
+
The example above introduces two issues:
|
548
|
+
- `on_complete` callback does not depend on the result of `user.referrals.create(referral_attrs)`. And it should - we only need to send the email only if referral is created. Solution - add the condition for the `on_complete` callback
|
549
|
+
- the number of user's referrals `user.referrals.count` is calculated inside `on_complete`, but it should be calculated within the transaction. Solution - calculate referrals count in transaction, extract its value into local variable and use that variable in the `on_complete` callback
|
550
|
+
|
551
|
+
#### Good
|
552
|
+
```ruby
|
553
|
+
trx do |c|
|
554
|
+
user = User.find(id)
|
555
|
+
referral = user.referrals.create(referral_attrs)
|
556
|
+
if referral.persisted?
|
557
|
+
total_referrals = user.referrals.count
|
558
|
+
c.on_complete do
|
559
|
+
Mailer.new_referral(user_id: user.id, total_referrals: total_referrals).deliver_later
|
560
|
+
end
|
561
|
+
end
|
562
|
+
end
|
563
|
+
```
|
564
|
+
|
565
|
+
## Make methods atomic.
|
566
|
+
|
567
|
+
You can make any method atomic by wrapping it into transaction using `#wrap_in_trx`. Example:
|
568
|
+
|
569
|
+
```ruby
|
570
|
+
class ApplicationRecord < ActiveRecord::Base
|
571
|
+
class << self
|
572
|
+
wrap_in_trx :find_or_create_by
|
573
|
+
wrap_in_trx :find_or_create_by!
|
574
|
+
end
|
575
|
+
|
576
|
+
wrap_in_trx def some_method
|
577
|
+
SomeRecord.first || SomeRecord.create
|
578
|
+
end
|
579
|
+
end
|
580
|
+
```
|
581
|
+
|
582
|
+
## Development
|
583
|
+
|
584
|
+
### Setup
|
585
|
+
|
586
|
+
- After checking out the repo, run `bin/setup` to install dependencies.
|
587
|
+
- Setup postgresql server with `serializable` transaction isolation level - you have to set `default_transaction_isolation` config option to `serializable` in your `postgresql.conf` file
|
588
|
+
- Create pg user and a database. This database will be used to run tests. When running console, this database will be used as a default database to connect to. Example:
|
589
|
+
```shell
|
590
|
+
sudo -u postgres createuser postgres --superuser
|
591
|
+
sudo -u postgres psql --command="CREATE DATABASE trx_ext_db OWNER postgres"
|
592
|
+
```
|
593
|
+
- Setup db connection settings. Copy config sample and edit it to match your created pg user and database:
|
594
|
+
```shell
|
595
|
+
cp spec/support/config/database.yml.sample spec/support/config/database.yml
|
596
|
+
```
|
597
|
+
|
598
|
+
Now you can run `bin/console` for an interactive prompt that will allow you to experiment.
|
599
|
+
|
600
|
+
### Tests
|
601
|
+
|
602
|
+
You can run tests for currently installed AR using `rspec` command. There is `bin/test_all_ar_versions` executable that allows you to run tests within all supported AR versions(see [TrxExt::SUPPORTED_AR_VERSIONS](lib/trx_ext/version.rb)) as well.
|
603
|
+
|
604
|
+
### Other
|
605
|
+
|
606
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
607
|
+
|
608
|
+
## Contributing
|
609
|
+
|
610
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/intale/trx_ext. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/intale/trx_ext/blob/master/CODE_OF_CONDUCT.md).
|
611
|
+
|
612
|
+
## License
|
613
|
+
|
614
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
615
|
+
|
616
|
+
## Code of Conduct
|
617
|
+
|
618
|
+
Everyone interacting in the TrxExt project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/intale/trx_ext/blob/master/CODE_OF_CONDUCT.md).
|
619
|
+
|
620
|
+
## TODO
|
621
|
+
|
622
|
+
- integrate GitHub Actions
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
# frozen_string_literal: true
|
4
|
+
|
5
|
+
require "bundler/setup"
|
6
|
+
require "trx_ext"
|
7
|
+
|
8
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
9
|
+
# with your gem easier. You can also use a different console, if you like.
|
10
|
+
|
11
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
12
|
+
# require "pry"
|
13
|
+
# Pry.start
|
14
|
+
|
15
|
+
require "irb"
|
16
|
+
require "logger"
|
17
|
+
require 'active_record'
|
18
|
+
require_relative '../spec/support/config'
|
19
|
+
require_relative '../spec/support/dummy_record'
|
20
|
+
|
21
|
+
ActiveRecord::Base.logger = ActiveSupport::Logger.new(STDOUT)
|
22
|
+
TrxExt.logger = ActiveSupport::Logger.new(STDOUT)
|
23
|
+
|
24
|
+
if Config.db_config
|
25
|
+
ActiveRecord::Base.establish_connection(Config.db_config)
|
26
|
+
DummyRecord.setup
|
27
|
+
end
|
28
|
+
|
29
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
# frozen_string_literal: true
|
4
|
+
|
5
|
+
require_relative '../lib/trx_ext/version'
|
6
|
+
|
7
|
+
TrxExt::SUPPORTED_AR_VERSIONS.each do |ar_version|
|
8
|
+
`rm Gemfile.lock`
|
9
|
+
Process.waitpid(Kernel.spawn({ 'AR_VERSION' => ar_version }, "bundle install --quiet", close_others: true))
|
10
|
+
Process.waitpid(Kernel.spawn({ 'AR_VERSION' => ar_version }, "rspec", close_others: true))
|
11
|
+
end
|