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.
@@ -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