double_entry 0.0.1.pre → 0.1.0.pre.pre.alpha
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Gem Version](https://badge.fury.io/rb/double_entry.svg)](http://badge.fury.io/rb/double_entry)
|
4
|
+
[![Build Status](https://travis-ci.org/envato/double_entry.svg)](https://travis-ci.org/envato/double_entry)
|
5
|
+
[![Code Climate](https://codeclimate.com/github/envato/double_entry.png)](https://codeclimate.com/github/envato/double_entry)
|
6
|
+
|
7
|
+
![Show me the Money](http://24.media.tumblr.com/tumblr_m3bwbqNJIG1rrgbmqo1_500.gif)
|
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)
|