em-pg-client-helper 0.6.0 → 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.
- data/lib/em-pg-client-helper.rb +21 -0
- data/lib/em-pg-client-helper/transaction.rb +41 -26
- data/spec/db_transaction_spec.rb +73 -26
- metadata +4 -4
data/lib/em-pg-client-helper.rb
CHANGED
@@ -68,6 +68,24 @@ module PG::EM::Client::Helper
|
|
68
68
|
# the transaction to complete against, so you don't have to worry about
|
69
69
|
# that, either.
|
70
70
|
#
|
71
|
+
# @param opts [Hash] Zero or more options which change the behaviour of the
|
72
|
+
# transaction.
|
73
|
+
#
|
74
|
+
# @option opts [Symbol] :isolation An isolation level for the transaction.
|
75
|
+
# Valid values are `:serializable`, `:repeatable_read`,
|
76
|
+
# `:read_committed`, and `:read_uncommitted`. The last two of these
|
77
|
+
# are pointless to use and are included only for completeness, as
|
78
|
+
# PostgreSQL's default isolation level is `:read_committed`, and
|
79
|
+
# `:read_uncommitted` is equivalent to `:read_committed`.
|
80
|
+
#
|
81
|
+
# @option opts [TrueClass, FalseClass] :retry Whether or not to retry the
|
82
|
+
# transaction if it fails for one of a number of transaction-internal
|
83
|
+
# reasons.
|
84
|
+
#
|
85
|
+
# @option opts [TrueClass, FalseClass] :deferrable If set, enables the
|
86
|
+
# `DEFERRABLE` transaction mode. For details of what this is, see the
|
87
|
+
# `SET TRANSACTION` command documentation in the PostgreSQL manual.
|
88
|
+
#
|
71
89
|
# @param blk [Proc] code which will be executed within the context of the
|
72
90
|
# transaction. This block will be passed a
|
73
91
|
# {PG::EM::Client::Helper::Transaction} instance, which has methods to
|
@@ -78,6 +96,9 @@ module PG::EM::Client::Helper
|
|
78
96
|
# `#errback` to define what to do when the transaction succeeds or
|
79
97
|
# fails, respectively.
|
80
98
|
#
|
99
|
+
# @raise [ArgumentError] If an unrecognised value for the `:isolation`
|
100
|
+
# option is given.
|
101
|
+
#
|
81
102
|
# @note Due to the way that transactions detect when they are completed,
|
82
103
|
# every deferrable in the scope of the transaction must be generated
|
83
104
|
# by the transaction. That is, you cannot use objects other than the
|
@@ -4,28 +4,56 @@ class PG::EM::Client::Helper::Transaction
|
|
4
4
|
|
5
5
|
# Create a new transaction. You shouldn't have to call this yourself;
|
6
6
|
# `db_transaction` should create one and pass it to your block.
|
7
|
-
|
7
|
+
#
|
8
|
+
# @param conn [PG::EM::Connection] The connection to execute all commands
|
9
|
+
# against. If using a connection pool, this connection needs to have
|
10
|
+
# been taken out of the pool (using something like `#hold_deferred`) so
|
11
|
+
# that no other concurrently-operating code can accidentally send
|
12
|
+
# queries down the connection (that would be, in a word, *bad*).
|
13
|
+
#
|
14
|
+
# @param opts [Hash] Zero or more optional parameters that adjust the
|
15
|
+
# initial state of the transaction. For full details of the available
|
16
|
+
# options, see {PG::EM::Client::Helper#db_transaction}.
|
17
|
+
#
|
18
|
+
# @raise [ArgumentError] If an unknown isolation level is specified.
|
19
|
+
#
|
20
|
+
def initialize(conn, opts = {}, &blk)
|
8
21
|
@conn = conn
|
9
22
|
@opts = opts
|
10
23
|
# This can be `nil` if the txn is in progress, or it will be
|
11
24
|
# true or false to indicate success/failure of the txn
|
12
25
|
@committed = nil
|
13
|
-
@retryable =
|
26
|
+
@retryable = opts[:retry]
|
14
27
|
|
15
28
|
DeferrableGroup.new do |dg|
|
16
29
|
@dg = dg
|
17
30
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
31
|
+
begin_query = case opts[:isolation]
|
32
|
+
when :serializable
|
33
|
+
"BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE"
|
34
|
+
when :repeatable_read
|
35
|
+
"BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ"
|
36
|
+
when :read_committed
|
37
|
+
"BEGIN TRANSACTION ISOLATION LEVEL READ COMMITTED"
|
38
|
+
when :read_uncommitted
|
39
|
+
"BEGIN TRANSACTION ISOLATION LEVEL READ UNCOMMITTED"
|
40
|
+
when nil
|
41
|
+
"BEGIN"
|
42
|
+
else
|
43
|
+
raise ArgumentError,
|
44
|
+
"Unknown value for :isolation option: #{opts[:isolation].inspect}"
|
45
|
+
end
|
46
|
+
|
47
|
+
if opts[:deferrable]
|
48
|
+
begin_query += " DEFERRABLE"
|
49
|
+
end
|
50
|
+
|
51
|
+
exec(begin_query) do
|
22
52
|
begin
|
23
53
|
blk.call(self)
|
24
54
|
rescue StandardError => ex
|
25
55
|
rollback(ex)
|
26
56
|
end
|
27
|
-
end.errback do |ex|
|
28
|
-
rollback(ex)
|
29
57
|
end
|
30
58
|
end.callback do
|
31
59
|
rollback(RuntimeError.new("txn.commit was not called")) unless @committed
|
@@ -43,18 +71,6 @@ class PG::EM::Client::Helper::Transaction
|
|
43
71
|
end
|
44
72
|
end
|
45
73
|
|
46
|
-
# Mark the transaction as requiring the serializable isolation level.
|
47
|
-
#
|
48
|
-
# @param retryable [TrueClass, FalseClass] Whether or not the transaction
|
49
|
-
# should be retried if some sort of transaction-level failure occurs.
|
50
|
-
# Be careful enabling this, as the entire block will be re-run, including
|
51
|
-
# any code that creates side-effects elsewhere.
|
52
|
-
#
|
53
|
-
def serializable(retryable = false, &blk)
|
54
|
-
@retryable = retryable
|
55
|
-
exec("SET TRANSACTION ISOLATION LEVEL SERIALIZABLE", &blk)
|
56
|
-
end
|
57
|
-
|
58
74
|
# Signal the database to commit this transaction. You must do this
|
59
75
|
# once you've completed your queries, it won't be called automatically
|
60
76
|
# for you. Once you've committed the transaction, you cannot use it
|
@@ -64,13 +80,15 @@ class PG::EM::Client::Helper::Transaction
|
|
64
80
|
def commit
|
65
81
|
if @committed.nil?
|
66
82
|
trace_query("COMMIT")
|
67
|
-
@conn.exec_defer("COMMIT").tap do |df|
|
83
|
+
@conn.exec_defer("COMMIT", []).tap do |df|
|
68
84
|
@dg.add(df)
|
69
85
|
end.callback do
|
70
86
|
@committed = true
|
71
87
|
@dg.close
|
72
88
|
end.errback do |ex|
|
73
|
-
|
89
|
+
@committed = false
|
90
|
+
@dg.fail(ex)
|
91
|
+
@dg.close
|
74
92
|
end
|
75
93
|
end
|
76
94
|
end
|
@@ -82,10 +100,7 @@ class PG::EM::Client::Helper::Transaction
|
|
82
100
|
#
|
83
101
|
def rollback(ex)
|
84
102
|
if @committed.nil?
|
85
|
-
|
86
|
-
@conn.exec_defer("ROLLBACK").tap do |df|
|
87
|
-
@dg.add(df)
|
88
|
-
end.callback do
|
103
|
+
exec("ROLLBACK") do
|
89
104
|
@committed = false
|
90
105
|
@dg.fail(ex)
|
91
106
|
@dg.close
|
data/spec/db_transaction_spec.rb
CHANGED
@@ -3,12 +3,12 @@ require_relative './spec_helper'
|
|
3
3
|
describe "PG::EM::Client::Helper#db_transaction" do
|
4
4
|
let(:mock_conn) { double(PG::EM::Client) }
|
5
5
|
|
6
|
-
def expect_query_failure(q, args=
|
6
|
+
def expect_query_failure(q, args=[], err=nil, exec_time = 0.001)
|
7
7
|
err ||= RuntimeError.new("Dummy failure")
|
8
8
|
expect_query(q, args, exec_time, :fail, err)
|
9
9
|
end
|
10
10
|
|
11
|
-
def expect_query(q, args=
|
11
|
+
def expect_query(q, args=[], exec_time = 0.001, disposition = :succeed, *disp_opts)
|
12
12
|
df = EM::DefaultDeferrable.new
|
13
13
|
|
14
14
|
expect(mock_conn)
|
@@ -22,8 +22,8 @@ describe "PG::EM::Client::Helper#db_transaction" do
|
|
22
22
|
end
|
23
23
|
end
|
24
24
|
|
25
|
-
def in_transaction(&blk)
|
26
|
-
db_transaction(mock_conn, &blk).callback { EM.stop }.errback { EM.stop }
|
25
|
+
def in_transaction(*args, &blk)
|
26
|
+
db_transaction(mock_conn, *args, &blk).callback { EM.stop }.errback { EM.stop }
|
27
27
|
end
|
28
28
|
|
29
29
|
def in_em
|
@@ -53,11 +53,10 @@ describe "PG::EM::Client::Helper#db_transaction" do
|
|
53
53
|
end
|
54
54
|
end
|
55
55
|
|
56
|
-
it "
|
56
|
+
it "doesn't roll back if COMMIT fails" do
|
57
57
|
in_em do
|
58
58
|
expect_query("BEGIN")
|
59
59
|
expect_query_failure("COMMIT")
|
60
|
-
expect_query("ROLLBACK")
|
61
60
|
in_transaction do |txn|
|
62
61
|
txn.commit
|
63
62
|
end
|
@@ -135,6 +134,30 @@ describe "PG::EM::Client::Helper#db_transaction" do
|
|
135
134
|
end
|
136
135
|
end
|
137
136
|
|
137
|
+
it "is robust against having an unrelated deferrable in the chain" do
|
138
|
+
in_em do
|
139
|
+
expect_query("BEGIN")
|
140
|
+
expect_query('INSERT INTO "foo" ("bar") VALUES ($1)', ['baz'])
|
141
|
+
expect_query('INSERT INTO "foo" ("bar") VALUES ($1)', ['wombat'])
|
142
|
+
expect_query('INSERT INTO "foo" ("bar") VALUES ($1)', ['quux'])
|
143
|
+
expect_query("COMMIT")
|
144
|
+
|
145
|
+
in_transaction do |txn|
|
146
|
+
txn.insert("foo", :bar => 'baz') do
|
147
|
+
txn.insert("foo", :bar => 'wombat') do
|
148
|
+
df = ::EM::DefaultDeferrable.new
|
149
|
+
df.callback do
|
150
|
+
txn.insert("foo", :bar => 'quux') do
|
151
|
+
txn.commit
|
152
|
+
end
|
153
|
+
end
|
154
|
+
df.succeed
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
138
161
|
it "doesn't COMMIT if we rolled back" do
|
139
162
|
in_em do
|
140
163
|
expect_query("BEGIN")
|
@@ -160,22 +183,51 @@ describe "PG::EM::Client::Helper#db_transaction" do
|
|
160
183
|
end
|
161
184
|
end
|
162
185
|
|
186
|
+
it "uses SERIALIZABLE if we ask nicely" do
|
187
|
+
in_em do
|
188
|
+
expect_query("BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE")
|
189
|
+
expect_query("COMMIT")
|
190
|
+
|
191
|
+
in_transaction(:isolation => :serializable) do |txn|
|
192
|
+
txn.commit
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
it "uses REPEATABLE READ if we ask nicely" do
|
198
|
+
in_em do
|
199
|
+
expect_query("BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ")
|
200
|
+
expect_query("COMMIT")
|
201
|
+
|
202
|
+
in_transaction(:isolation => :repeatable_read) do |txn|
|
203
|
+
txn.commit
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
it "uses DEFERRABLE if we ask nicely" do
|
209
|
+
in_em do
|
210
|
+
expect_query("BEGIN DEFERRABLE")
|
211
|
+
expect_query("COMMIT")
|
212
|
+
|
213
|
+
in_transaction(:deferrable => true) do |txn|
|
214
|
+
txn.commit
|
215
|
+
end
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
163
219
|
it "retries if it gets an error during the transaction" do
|
164
220
|
in_em do
|
165
|
-
expect_query("BEGIN")
|
166
|
-
expect_query("SET TRANSACTION ISOLATION LEVEL SERIALIZABLE", [])
|
221
|
+
expect_query("BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE")
|
167
222
|
expect_query_failure('INSERT INTO "foo" ("bar") VALUES ($1)', ["baz"], PG::TRSerializationFailure.new("OMFG!"))
|
168
223
|
expect_query("ROLLBACK")
|
169
|
-
expect_query("BEGIN")
|
170
|
-
expect_query("SET TRANSACTION ISOLATION LEVEL SERIALIZABLE", [])
|
224
|
+
expect_query("BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE")
|
171
225
|
expect_query('INSERT INTO "foo" ("bar") VALUES ($1)', ["baz"])
|
172
226
|
expect_query("COMMIT")
|
173
227
|
|
174
|
-
in_transaction do |txn|
|
175
|
-
txn.
|
176
|
-
txn.
|
177
|
-
txn.commit
|
178
|
-
end
|
228
|
+
in_transaction(:isolation => :serializable, :retry => true) do |txn|
|
229
|
+
txn.insert("foo", :bar => 'baz') do
|
230
|
+
txn.commit
|
179
231
|
end
|
180
232
|
end
|
181
233
|
end
|
@@ -183,21 +235,16 @@ describe "PG::EM::Client::Helper#db_transaction" do
|
|
183
235
|
|
184
236
|
it "retries if it gets an error on commit" do
|
185
237
|
in_em do
|
186
|
-
expect_query("BEGIN")
|
187
|
-
expect_query("SET TRANSACTION ISOLATION LEVEL SERIALIZABLE", [])
|
238
|
+
expect_query("BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE")
|
188
239
|
expect_query('INSERT INTO "foo" ("bar") VALUES ($1)', ["baz"])
|
189
|
-
expect_query_failure("COMMIT",
|
190
|
-
expect_query("
|
191
|
-
expect_query("BEGIN")
|
192
|
-
expect_query("SET TRANSACTION ISOLATION LEVEL SERIALIZABLE", [])
|
240
|
+
expect_query_failure("COMMIT", [], PG::TRSerializationFailure.new("OMFG!"))
|
241
|
+
expect_query("BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE")
|
193
242
|
expect_query('INSERT INTO "foo" ("bar") VALUES ($1)', ["baz"])
|
194
243
|
expect_query("COMMIT")
|
195
244
|
|
196
|
-
in_transaction do |txn|
|
197
|
-
txn.
|
198
|
-
txn.
|
199
|
-
txn.commit
|
200
|
-
end
|
245
|
+
in_transaction(:isolation => :serializable, :retry => true) do |txn|
|
246
|
+
txn.insert("foo", :bar => 'baz') do
|
247
|
+
txn.commit
|
201
248
|
end
|
202
249
|
end
|
203
250
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: em-pg-client-helper
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 1.0.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2015-01-
|
12
|
+
date: 2015-01-22 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: git-version-bump
|
@@ -261,7 +261,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
261
261
|
version: '0'
|
262
262
|
segments:
|
263
263
|
- 0
|
264
|
-
hash:
|
264
|
+
hash: 4034289824364546696
|
265
265
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
266
266
|
none: false
|
267
267
|
requirements:
|
@@ -270,7 +270,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
270
270
|
version: '0'
|
271
271
|
segments:
|
272
272
|
- 0
|
273
|
-
hash:
|
273
|
+
hash: 4034289824364546696
|
274
274
|
requirements: []
|
275
275
|
rubyforge_project:
|
276
276
|
rubygems_version: 1.8.23
|