em-pg-client-helper 0.6.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
- def initialize(conn, opts, &blk)
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 = false
26
+ @retryable = opts[:retry]
14
27
 
15
28
  DeferrableGroup.new do |dg|
16
29
  @dg = dg
17
30
 
18
- trace_query("BEGIN")
19
- @conn.exec_defer("BEGIN").tap do |df|
20
- @dg.add(df)
21
- end.callback do
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
- rollback(ex)
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
- trace_query("ROLLBACK")
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
@@ -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=nil, err=nil, exec_time = 0.001)
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=nil, exec_time = 0.001, disposition = :succeed, *disp_opts)
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 "rolls back if COMMIT fails" do
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.serializable(true) do
176
- txn.insert("foo", :bar => 'baz') do
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", nil, PG::TRSerializationFailure.new("OMFG!"))
190
- expect_query("ROLLBACK")
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.serializable(true) do
198
- txn.insert("foo", :bar => 'baz') do
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.6.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-21 00:00:00.000000000 Z
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: -3912850109456819706
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: -3912850109456819706
273
+ hash: 4034289824364546696
274
274
  requirements: []
275
275
  rubyforge_project:
276
276
  rubygems_version: 1.8.23