double_entry 0.0.1.pre → 0.1.0.pre.pre.alpha
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 +13 -5
- data/.gitignore +5 -6
- data/.rspec +1 -0
- data/.travis.yml +19 -0
- data/.yardopts +2 -0
- data/Gemfile +0 -1
- data/LICENSE.md +19 -0
- data/README.md +221 -14
- data/Rakefile +12 -0
- data/double_entry.gemspec +30 -15
- data/gemfiles/Gemfile.rails-3.2.0 +5 -0
- data/gemfiles/Gemfile.rails-4.0.0 +5 -0
- data/gemfiles/Gemfile.rails-4.1.0 +5 -0
- data/lib/active_record/locking_extensions.rb +61 -0
- data/lib/double_entry.rb +267 -2
- data/lib/double_entry/account.rb +82 -0
- data/lib/double_entry/account_balance.rb +31 -0
- data/lib/double_entry/aggregate.rb +118 -0
- data/lib/double_entry/aggregate_array.rb +65 -0
- data/lib/double_entry/configurable.rb +52 -0
- data/lib/double_entry/day_range.rb +38 -0
- data/lib/double_entry/hour_range.rb +40 -0
- data/lib/double_entry/line.rb +147 -0
- data/lib/double_entry/line_aggregate.rb +37 -0
- data/lib/double_entry/line_check.rb +118 -0
- data/lib/double_entry/locking.rb +187 -0
- data/lib/double_entry/month_range.rb +92 -0
- data/lib/double_entry/reporting.rb +16 -0
- data/lib/double_entry/time_range.rb +55 -0
- data/lib/double_entry/time_range_array.rb +43 -0
- data/lib/double_entry/transfer.rb +70 -0
- data/lib/double_entry/version.rb +3 -1
- data/lib/double_entry/week_range.rb +99 -0
- data/lib/double_entry/year_range.rb +39 -0
- data/lib/generators/double_entry/install/install_generator.rb +22 -0
- data/lib/generators/double_entry/install/templates/migration.rb +68 -0
- data/script/jack_hammer +201 -0
- data/script/setup.sh +8 -0
- data/spec/active_record/locking_extensions_spec.rb +54 -0
- data/spec/double_entry/account_balance_spec.rb +8 -0
- data/spec/double_entry/account_spec.rb +23 -0
- data/spec/double_entry/aggregate_array_spec.rb +75 -0
- data/spec/double_entry/aggregate_spec.rb +168 -0
- data/spec/double_entry/double_entry_spec.rb +391 -0
- data/spec/double_entry/line_aggregate_spec.rb +8 -0
- data/spec/double_entry/line_check_spec.rb +88 -0
- data/spec/double_entry/line_spec.rb +72 -0
- data/spec/double_entry/locking_spec.rb +154 -0
- data/spec/double_entry/month_range_spec.rb +131 -0
- data/spec/double_entry/reporting_spec.rb +25 -0
- data/spec/double_entry/time_range_array_spec.rb +149 -0
- data/spec/double_entry/time_range_spec.rb +43 -0
- data/spec/double_entry/week_range_spec.rb +88 -0
- data/spec/generators/double_entry/install/install_generator_spec.rb +33 -0
- data/spec/spec_helper.rb +47 -0
- data/spec/support/accounts.rb +26 -0
- data/spec/support/blueprints.rb +34 -0
- data/spec/support/database.example.yml +16 -0
- data/spec/support/database.travis.yml +18 -0
- data/spec/support/double_entry_spec_helper.rb +19 -0
- data/spec/support/reporting_configuration.rb +6 -0
- data/spec/support/schema.rb +71 -0
- metadata +277 -18
- data/LICENSE.txt +0 -22
checksums.yaml
CHANGED
@@ -1,7 +1,15 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
|
2
|
+
!binary "U0hBMQ==":
|
3
|
+
metadata.gz: !binary |-
|
4
|
+
Y2ZlMDRhZWIzMzM2NDg0MmRlNmNhY2ZkNzY0Nzc3NjhlYWMwNjJiMw==
|
5
|
+
data.tar.gz: !binary |-
|
6
|
+
NDFhYzZhYmY1Zjg1NzYxMzM4NzA1NzIzMWFkMTJmMDBlYzRjOGE4Yg==
|
5
7
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
|
8
|
+
metadata.gz: !binary |-
|
9
|
+
MzZmNTNmOGVlYTgyN2I4MjljZGU5ZGZlODJkMTMyM2JlNWQyMmY3OTQxY2Fh
|
10
|
+
NDVjZmYxNmQ1OGNiMTA5ODUwM2U5ZjJlMmEzMjY4MmMyOWY4Y2M0ZmI5YzJj
|
11
|
+
OTU5MDQ0MzMxODk4NGNjMGI3Yzk2ZjYxZjc4ODkyODU0MGNhM2Y=
|
12
|
+
data.tar.gz: !binary |-
|
13
|
+
MDcxOWE3OTFkNzQ3ODk1MTVhYzFjYTlmMWNiNGM4ZmU5N2UzMDc3M2U5Zjkz
|
14
|
+
YTU5MDVhM2JmODQ0ZTgzYmVkZjdiNDRhZWE0ZDI0YjQwYjBkNWEzZGEzOGQw
|
15
|
+
YWE3OTE2ODlmODc3ZjY2YTE1ODBjYzcxNmY4M2M0MTJkMTVhMWY=
|
data/.gitignore
CHANGED
@@ -3,7 +3,7 @@
|
|
3
3
|
.bundle
|
4
4
|
.config
|
5
5
|
.yardoc
|
6
|
-
|
6
|
+
.ruby-version
|
7
7
|
InstalledFiles
|
8
8
|
_yardoc
|
9
9
|
coverage
|
@@ -12,11 +12,10 @@ lib/bundler/man
|
|
12
12
|
pkg
|
13
13
|
rdoc
|
14
14
|
spec/reports
|
15
|
+
spec/support/database.yml
|
15
16
|
test/tmp
|
16
17
|
test/version_tmp
|
17
18
|
tmp
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
*.a
|
22
|
-
mkmf.log
|
19
|
+
bin/
|
20
|
+
log/
|
21
|
+
/Gemfile.lock
|
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--colour
|
data/.travis.yml
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
language: ruby
|
2
|
+
rvm:
|
3
|
+
- 2.1.2
|
4
|
+
- 2.0.0
|
5
|
+
- 1.9.3
|
6
|
+
env:
|
7
|
+
- DB=mysql
|
8
|
+
- DB=postgres
|
9
|
+
before_script:
|
10
|
+
- cp spec/support/database.travis.yml spec/support/database.yml
|
11
|
+
- mysql -e 'create database double_entry_test;'
|
12
|
+
- psql -c 'create database double_entry_test;' -U postgres
|
13
|
+
script:
|
14
|
+
- rake spec
|
15
|
+
- ruby script/jack_hammer -t 2000
|
16
|
+
gemfile:
|
17
|
+
- gemfiles/Gemfile.rails-3.2.0
|
18
|
+
- gemfiles/Gemfile.rails-4.0.0
|
19
|
+
- gemfiles/Gemfile.rails-4.1.0
|
data/.yardopts
ADDED
data/Gemfile
CHANGED
data/LICENSE.md
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
Copyright © 2014 Envato Pty Ltd
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
5
|
+
in the Software without restriction, including without limitation the rights
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
8
|
+
furnished to do so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in
|
11
|
+
all copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
THE SOFTWARE.
|
data/README.md
CHANGED
@@ -1,29 +1,236 @@
|
|
1
1
|
# DoubleEntry
|
2
2
|
|
3
|
-
|
3
|
+
[](http://badge.fury.io/rb/double_entry)
|
4
|
+
[](https://travis-ci.org/envato/double_entry)
|
5
|
+
[](https://codeclimate.com/github/envato/double_entry)
|
6
|
+
|
7
|
+

|
8
|
+
|
9
|
+
Keep track of all the monies!
|
10
|
+
|
11
|
+
DoubleEntry is an accounting system based on the principles of a
|
12
|
+
[Double-entry Bookkeeping](http://en.wikipedia.org/wiki/Double-entry_bookkeeping_system)
|
13
|
+
system. While this gem acts like a double-entry bookkeeping system, as it creates
|
14
|
+
two entries in the database for each transfer, it does *not* enforce accounting rules.
|
15
|
+
|
16
|
+
DoubleEntry uses the Money gem to avoid floating point rounding errors.
|
17
|
+
|
18
|
+
## Compatibility
|
19
|
+
|
20
|
+
DoubleEntry has been tested with:
|
21
|
+
|
22
|
+
Ruby Versions: 1.9.3, 2.0.0, 2.1.2
|
23
|
+
|
24
|
+
Rails Versions: Rails 3.2.x, 4.0.x, 4.1.x
|
25
|
+
|
26
|
+
**Databases Supported:**
|
27
|
+
* MySQL
|
28
|
+
* PostgreSQL
|
4
29
|
|
5
30
|
## Installation
|
6
31
|
|
7
|
-
|
32
|
+
In your application's `Gemfile`, add:
|
8
33
|
|
9
34
|
gem 'double_entry'
|
10
35
|
|
11
|
-
|
36
|
+
Then run:
|
37
|
+
|
38
|
+
bundle
|
39
|
+
rails generate double_entry:install
|
40
|
+
|
41
|
+
Run migration files:
|
42
|
+
|
43
|
+
rake db:migrate
|
44
|
+
|
45
|
+
|
46
|
+
## Interface
|
47
|
+
|
48
|
+
The entire API for recording financial transactions is available through a few
|
49
|
+
methods in the **DoubleEntry** module. For full details on
|
50
|
+
what the API provides, please view the documentation on these methods.
|
51
|
+
|
52
|
+
A configuration file should be used to define a set of accounts, and potential
|
53
|
+
transfers between those accounts. See the Configuration section for more details.
|
54
|
+
|
55
|
+
|
56
|
+
### Accounts
|
57
|
+
|
58
|
+
Money is kept in Accounts.
|
59
|
+
|
60
|
+
Each Account has a scope, which is used to subdivide the account into smaller
|
61
|
+
accounts. For example, an account can be scoped by user to ensure that each
|
62
|
+
user has their own individual account.
|
63
|
+
|
64
|
+
Scoping accounts is recommended. Unscoped accounts may perform more slowly
|
65
|
+
than scoped accounts due to lock contention.
|
66
|
+
|
67
|
+
To get a particular account:
|
68
|
+
|
69
|
+
account = DoubleEntry.account(:spending, :scope => user)
|
70
|
+
|
71
|
+
(This actually returns an Account::Instance object.)
|
72
|
+
|
73
|
+
See **DoubleEntry::Account** for more info.
|
74
|
+
|
75
|
+
|
76
|
+
### Balances
|
77
|
+
|
78
|
+
Calling:
|
79
|
+
|
80
|
+
account.balance
|
81
|
+
|
82
|
+
will return the current balance for an account as a Money object.
|
83
|
+
|
84
|
+
|
85
|
+
### Transfers
|
86
|
+
|
87
|
+
To transfer money between accounts:
|
88
|
+
|
89
|
+
DoubleEntry.transfer(20.dollars, :from => account_a, :to => account_b, :code => :purchase)
|
90
|
+
|
91
|
+
The possible transfers, and their codes, should be defined in the configuration.
|
92
|
+
|
93
|
+
See **DoubleEntry::Transfer** for more info.
|
94
|
+
|
95
|
+
|
96
|
+
### Locking
|
97
|
+
|
98
|
+
If you're doing more than one transfer in a single financial transaction, or
|
99
|
+
you're doing other database operations along with the transfer, you'll need to
|
100
|
+
manually lock the accounts you're using:
|
101
|
+
|
102
|
+
DoubleEntry.lock_accounts(account_a, account_b) do
|
103
|
+
# Do some other stuff in here...
|
104
|
+
DoubleEntry.transfer(20.dollars, :from => account_a, :to => account_b, :code => :purchase)
|
105
|
+
end
|
106
|
+
|
107
|
+
The lock_accounts call generates a database transaction, which must be the
|
108
|
+
outermost transaction.
|
109
|
+
|
110
|
+
See **DoubleEntry::Locking** for more info.
|
111
|
+
|
112
|
+
|
113
|
+
## Implementation
|
114
|
+
|
115
|
+
All transfers and balances are stored in the lines table. As this is a
|
116
|
+
double-entry accounting system, each transfer generates two lines table
|
117
|
+
entries: one for the source account, and one for the destination.
|
118
|
+
|
119
|
+
Lines table entries also store the running balance for the account. To retrieve
|
120
|
+
the current balance for an account, we find the most recent lines table entry
|
121
|
+
for it.
|
122
|
+
|
123
|
+
See **DoubleEntry::Line** for more info.
|
124
|
+
|
125
|
+
AccountBalance records cache the current balance for each Account, and are used
|
126
|
+
to perform database level locking.
|
127
|
+
|
128
|
+
## Configuration
|
129
|
+
|
130
|
+
A configuration file should be used to define a set of accounts, optional scopes on
|
131
|
+
the accounts, and permitted transfers between those accounts.
|
132
|
+
|
133
|
+
The configuration file should be kept in your application's load path. For example,
|
134
|
+
*config/initializers/double_entry.rb*
|
135
|
+
|
136
|
+
For example, the following specifies two accounts, account_a and account_b.
|
137
|
+
Each account is scoped by User (where User is an object with an ID), meaning
|
138
|
+
each user can have their own account of each type.
|
139
|
+
|
140
|
+
This configuration also specifies that money can be transferred between the two accounts.
|
141
|
+
|
142
|
+
```ruby
|
143
|
+
require 'double_entry'
|
144
|
+
|
145
|
+
DoubleEntry.accounts = DoubleEntry::Account::Set.new.tap do |accounts|
|
146
|
+
user_scope = lambda do |user_identifier|
|
147
|
+
if user_identifier.is_a?(User)
|
148
|
+
user_identifier.id
|
149
|
+
else
|
150
|
+
user_identifier
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
accounts << DoubleEntry::Account.new(identifier: :savings, scope_identifier: user_scope, positive_only: true)
|
155
|
+
accounts << DoubleEntry::Account.new(identifier: :checking, scope_identifier: user_scope)
|
156
|
+
end
|
157
|
+
|
158
|
+
DoubleEntry.transfers = DoubleEntry::Transfer::Set.new.tap do |transfers|
|
159
|
+
transfers << DoubleEntry::Transfer.new(from: :checking, to: :savings, code: :deposit)
|
160
|
+
transfers << DoubleEntry::Transfer.new(from: :savings, to: :checking, code: :withdraw)
|
161
|
+
end
|
162
|
+
```
|
163
|
+
|
164
|
+
## Jackhammer
|
165
|
+
|
166
|
+
Run a concurrency test on the code.
|
167
|
+
|
168
|
+
This spawns a bunch of processes, and does random transactions between a set
|
169
|
+
of accounts, then validates that all the numbers add up at the end.
|
170
|
+
|
171
|
+
You can also tell out it to flush out the account balances table at regular
|
172
|
+
intervals, to validate that new account balances records get created with the
|
173
|
+
correct balances from the lines table.
|
174
|
+
|
175
|
+
./script/jack_hammer -t 20
|
176
|
+
Cleaning out the database...
|
177
|
+
Setting up 5 accounts...
|
178
|
+
Spawning 20 processes...
|
179
|
+
Flushing balances
|
180
|
+
Process 1 running 1 transfers...
|
181
|
+
Process 0 running 1 transfers...
|
182
|
+
Process 3 running 1 transfers...
|
183
|
+
Process 2 running 1 transfers...
|
184
|
+
Process 4 running 1 transfers...
|
185
|
+
Process 5 running 1 transfers...
|
186
|
+
Process 6 running 1 transfers...
|
187
|
+
Process 7 running 1 transfers...
|
188
|
+
Process 8 running 1 transfers...
|
189
|
+
Process 9 running 1 transfers...
|
190
|
+
Process 10 running 1 transfers...
|
191
|
+
Process 11 running 1 transfers...
|
192
|
+
Process 12 running 1 transfers...
|
193
|
+
Process 13 running 1 transfers...
|
194
|
+
Process 14 running 1 transfers...
|
195
|
+
Process 16 running 1 transfers...
|
196
|
+
Process 15 running 1 transfers...
|
197
|
+
Process 17 running 1 transfers...
|
198
|
+
Process 19 running 1 transfers...
|
199
|
+
Process 18 running 1 transfers...
|
200
|
+
Reconciling...
|
201
|
+
All the Line records were written, FTW!
|
202
|
+
All accounts reconciled, FTW!
|
203
|
+
Done successfully :)
|
204
|
+
|
205
|
+
## Future Direction
|
206
|
+
|
207
|
+
No immediate to-do's.
|
208
|
+
|
209
|
+
## Development Environment Setup
|
210
|
+
|
211
|
+
1. Clone this repo.
|
212
|
+
|
213
|
+
git clone git@github.com:envato/double_entry.git && cd double_entry
|
214
|
+
|
215
|
+
2. Run the included setup script to install the gem dependencies.
|
216
|
+
|
217
|
+
./script/setup.sh
|
218
|
+
|
219
|
+
3. Install MySQL and PostgreSQL. The tests run using both databases.
|
220
|
+
4. Create a database in MySQL.
|
221
|
+
|
222
|
+
mysql -u root -e 'create database double_entry_test;'
|
12
223
|
|
13
|
-
|
224
|
+
5. Create a database in PostgreSQL.
|
14
225
|
|
15
|
-
|
226
|
+
psql -c 'create database double_entry_test;' -U postgres
|
16
227
|
|
17
|
-
|
228
|
+
6. Specify how the tests should connect to the database
|
18
229
|
|
19
|
-
|
230
|
+
cp spec/support/database.example.yml spec/support/database.yml
|
231
|
+
vim spec/support/database.yml
|
20
232
|
|
21
|
-
|
233
|
+
7. Run the tests
|
22
234
|
|
23
|
-
|
235
|
+
bundle exec rake
|
24
236
|
|
25
|
-
1. Fork it ( https://github.com/[my-github-username]/double_entry/fork )
|
26
|
-
2. Create your feature branch (`git checkout -b my-new-feature`)
|
27
|
-
3. Commit your changes (`git commit -am 'Add some feature'`)
|
28
|
-
4. Push to the branch (`git push origin my-new-feature`)
|
29
|
-
5. Create a new Pull Request
|
data/Rakefile
CHANGED
@@ -1,2 +1,14 @@
|
|
1
|
+
require "rspec/core/rake_task"
|
1
2
|
require "bundler/gem_tasks"
|
2
3
|
|
4
|
+
RSpec::Core::RakeTask.new(:spec) do |t|
|
5
|
+
t.verbose = false
|
6
|
+
end
|
7
|
+
|
8
|
+
task :default do
|
9
|
+
%w(mysql postgres).each do |db|
|
10
|
+
puts "Running tests with `DB=#{db}`"
|
11
|
+
ENV['DB'] = db
|
12
|
+
Rake::Task["spec"].execute
|
13
|
+
end
|
14
|
+
end
|
data/double_entry.gemspec
CHANGED
@@ -1,22 +1,37 @@
|
|
1
|
-
#
|
1
|
+
# encoding: utf-8
|
2
|
+
|
2
3
|
lib = File.expand_path('../lib', __FILE__)
|
3
4
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
|
+
|
4
6
|
require 'double_entry/version'
|
5
7
|
|
6
|
-
Gem::Specification.new do |
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
8
|
+
Gem::Specification.new do |gem|
|
9
|
+
gem.name = 'double_entry'
|
10
|
+
gem.version = DoubleEntry::VERSION
|
11
|
+
gem.authors = ['Anthony Sellitti', 'Keith Pitt', 'Martin Jagusch', 'Martin Spickermann', 'Mark Turnley', 'Orien Madgwick', 'Pete Yandall', 'Stephanie Staub']
|
12
|
+
gem.email = ['anthony.sellitti@envato.com', 'me@keithpitt.com', '_@mj.io', 'spickemann@gmail.com', 'mark@envato.com', '_@orien.io', 'pete@envato.com', 'staub.steph@gmail.com']
|
13
|
+
# gem.description = %q{}
|
14
|
+
gem.summary = 'Tools to build your double entry financial ledger'
|
15
|
+
gem.homepage = 'https://github.com/envato/double_entry'
|
16
|
+
|
17
|
+
gem.files = `git ls-files`.split($/)
|
18
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
19
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
20
|
+
gem.require_paths = ['lib']
|
14
21
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
22
|
+
gem.add_dependency 'money', '>= 5.1.0'
|
23
|
+
gem.add_dependency 'encapsulate_as_money'
|
24
|
+
gem.add_dependency 'activerecord', '>= 3.2.9'
|
25
|
+
gem.add_dependency 'activesupport', '>= 3.0.0'
|
26
|
+
gem.add_dependency 'railties', '>= 3.0.0'
|
19
27
|
|
20
|
-
|
21
|
-
|
28
|
+
gem.add_development_dependency 'rake'
|
29
|
+
gem.add_development_dependency 'mysql2'
|
30
|
+
gem.add_development_dependency 'pg'
|
31
|
+
gem.add_development_dependency 'rspec'
|
32
|
+
gem.add_development_dependency 'rspec-its'
|
33
|
+
gem.add_development_dependency 'database_cleaner'
|
34
|
+
gem.add_development_dependency 'generator_spec'
|
35
|
+
gem.add_development_dependency 'machinist'
|
36
|
+
gem.add_development_dependency 'timecop'
|
22
37
|
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
module ActiveRecord
|
3
|
+
|
4
|
+
# These methods are available as class methods on ActiveRecord::Base.
|
5
|
+
module LockingExtensions
|
6
|
+
|
7
|
+
# Execute the given block within a database transaction, and retry the
|
8
|
+
# transaction from the beginning if a RestartTransaction exception is raised.
|
9
|
+
def restartable_transaction(&block)
|
10
|
+
begin
|
11
|
+
transaction(&block)
|
12
|
+
rescue ActiveRecord::RestartTransaction
|
13
|
+
retry
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
# Execute the given block, and retry the current restartable transaction if a
|
18
|
+
# MySQL deadlock occurs.
|
19
|
+
def with_restart_on_deadlock
|
20
|
+
begin
|
21
|
+
yield
|
22
|
+
rescue ActiveRecord::StatementInvalid => exception
|
23
|
+
if exception.message =~ /deadlock/i
|
24
|
+
raise ActiveRecord::RestartTransaction
|
25
|
+
else
|
26
|
+
raise
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# Create the record, but ignore the exception if there's a duplicate.
|
32
|
+
def create_ignoring_duplicates!(*args)
|
33
|
+
# Error examples:
|
34
|
+
# PG::Error: ERROR: deadlock detected
|
35
|
+
# Mysql::Error: Deadlock found when trying to get lock
|
36
|
+
# PG::Error: ERROR: duplicate key value violates unique constraint
|
37
|
+
# Mysql2::Error: Duplicate entry 'keith' for key 'index_users_on_username': INSERT INTO `users...
|
38
|
+
begin
|
39
|
+
create!(*args)
|
40
|
+
rescue ActiveRecord::StatementInvalid => exception
|
41
|
+
if exception.message =~ /duplicate/i
|
42
|
+
# Just ignore it...someone else has already created the record.
|
43
|
+
elsif exception.message =~ /deadlock/i
|
44
|
+
# Somebody else is in the midst of creating the record. We'd better
|
45
|
+
# retry, so we ensure they're done before we move on.
|
46
|
+
retry
|
47
|
+
else
|
48
|
+
raise
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
|
55
|
+
# Raise this inside a restartable_transaction to retry the transaction from the beginning.
|
56
|
+
class RestartTransaction < RuntimeError
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
|
61
|
+
ActiveRecord::Base.extend(ActiveRecord::LockingExtensions)
|