em-pg-client-helper 0.2.0 → 0.3.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.
@@ -48,17 +48,16 @@ module PG::EM::Client::Helper
48
48
  #
49
49
  # Calling this method opens up a transaction (by executing `BEGIN`), and
50
50
  # then runs the supplied block, passing in a transaction object which you
51
- # can use to execute SQL commands. You *must* call `txn.commit` or
52
- # `txn.callback` yourself as your inner-most callback, otherwise you will
53
- # leave the connection in a weird limbo state.
51
+ # can use to execute SQL commands. Once the transaction is finished,
52
+ # `COMMIT` or `ROLLBACK` will be sent to the DB server to complete the
53
+ # transaction, depending on whether or not any errors (query failures or
54
+ # Ruby exceptions) appeared during the transaction. You can also
55
+ # manually call `txn.rollback(reason)` if you want to signal that the
56
+ # transaction should be rolled back.
54
57
  #
55
- # Since `db_transaction` returns a deferrable, you should use `#callback`
56
- # to specify what to run after the transaction completes.
57
- #
58
- # If an SQL error occurs during the transaction, the block's execution
59
- # will be aborted, a `ROLLBACK` will be executed, and the `#errback`
60
- # block (if defined) on the deferrable that `db_transaction` returns will
61
- # be executed (rather than the `#callback` block).
58
+ # You should use `#callback` and `#errback` against the deferrable that
59
+ # `db_transaction` returns to specify what to run after the transaction
60
+ # completes successfully or fails, respectively.
62
61
  #
63
62
  # Arguments:
64
63
  #
@@ -72,10 +71,7 @@ module PG::EM::Client::Helper
72
71
  # of the transaction. This block will be passed a
73
72
  # `PG::EM::Client::Helper::Transaction` instance, which has methods to
74
73
  # allow you to commit or rollback the transaction, and execute SQL
75
- # statements within the context of the transaction. This block *must*
76
- # return a deferrable. When that returned deferrable completes, a
77
- # COMMIT will be triggered automatically. If that deferrable fails,
78
- # a ROLLBACK will be sent, instead.
74
+ # statements within the context of the transaction.
79
75
  #
80
76
  # Returns a deferrable object, on which you can call `#callback` and
81
77
  # `#errback` to define what to do when the transaction succeeds or fails,
@@ -98,74 +94,8 @@ module PG::EM::Client::Helper
98
94
  def quote_identifier(id)
99
95
  "\"#{id.gsub(/"/, '""')}\""
100
96
  end
97
+ end
101
98
 
102
- class Transaction
103
- include ::PG::EM::Client::Helper
104
- include ::EventMachine::Deferrable
105
-
106
- # Create a new transaction. You shouldn't have to call this yourself;
107
- # `db_transaction` should create one and pass it to your block.
108
- def initialize(conn, opts, &blk)
109
- @conn = conn
110
- @opts = opts
111
- @active = true
112
-
113
- conn.exec_defer("BEGIN").callback do
114
- blk.call(self)
115
- end.errback { |ex| rollback(ex) }
116
- end
117
-
118
- # Signal the database to commit this transaction. You must do this
119
- # once you've completed your queries, it won't be called automatically
120
- # for you. Once you've committed the transaction, you cannot use it
121
- # again; if you execute a query against a committed transaction, an
122
- # exception will be raised.
123
- #
124
- def commit
125
- @conn.exec_defer("COMMIT").callback do
126
- self.succeed
127
- @active = false
128
- end.errback { |ex| rollback(ex) }
129
- end
130
-
131
- # Signal the database to abort this transaction. You only need to
132
- # call this method if you need to rollback for some business logic
133
- # reason -- a rollback will be automatically performed for you in the
134
- # event of a database error or other exception.
135
- #
136
- def rollback(ex)
137
- if @active
138
- @conn.exec_defer("ROLLBACK") do
139
- @active = false
140
- self.fail(ex)
141
- end
142
- end
143
- end
144
-
145
- # Insert a row of data into the database table `tbl`, using the keys
146
- # from the `params` hash as the field names, and the values from the
147
- # `params` hash as the field data. Once the query has completed,
148
- # `blk` will be called to allow the transaction to continue.
149
- #
150
- def insert(tbl, params, &blk)
151
- exec(*insert_sql(tbl, params), &blk)
152
- end
153
-
154
- # Execute an arbitrary block of SQL in `sql` within the transaction.
155
- # If you need to pass dynamic values to the query, those should be
156
- # given in `values`, and referenced in `sql` as `$1`, `$2`, etc. The
157
- # given block will be called if and when the query completes
158
- # successfully.
159
- #
160
- def exec(sql, values=[], &blk)
161
- unless @active
162
- raise RuntimeError,
163
- "Cannot execute a query in a transaction that has been closed"
164
- end
99
+ require_relative 'em-pg-client-helper/transaction'
100
+ require_relative 'em-pg-client-helper/deferrable_group'
165
101
 
166
- @conn.exec_defer(sql, values).
167
- errback { |ex| rollback(ex) }.
168
- tap { |df| df.callback(&blk) if blk }
169
- end
170
- end
171
- end
@@ -0,0 +1,43 @@
1
+ # Fire a callback/errback when all of a group of deferrables is finished.
2
+ #
3
+ # Essentially a "barrier" for deferrables; you define the set of
4
+ # deferrables you want to group together, and when they're all finished,
5
+ # only *then* does the callback(s) or errback(s) on this deferrable fire.
6
+ #
7
+ class PG::EM::Client::Helper::DeferrableGroup
8
+ include ::EventMachine::Deferrable
9
+
10
+ def initialize
11
+ @failed = false
12
+ @first_failure = nil
13
+ @outstanding = []
14
+ yield(self) if block_given?
15
+ end
16
+
17
+ def add(df)
18
+ @outstanding << df
19
+ df.callback { completed(df) }.errback { |ex| failed(df, ex) }
20
+ end
21
+
22
+ def completed(df)
23
+ @outstanding.delete(df)
24
+ maybe_done
25
+ end
26
+
27
+ def failed(df, ex)
28
+ @first_failure ||= ex
29
+ @failed = true
30
+ completed(df)
31
+ end
32
+
33
+ private
34
+ def maybe_done
35
+ if @outstanding.empty?
36
+ if @failed
37
+ fail(@first_failure)
38
+ else
39
+ succeed
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,77 @@
1
+ class PG::EM::Client::Helper::Transaction
2
+ include ::PG::EM::Client::Helper
3
+ include ::EventMachine::Deferrable
4
+
5
+ # Create a new transaction. You shouldn't have to call this yourself;
6
+ # `db_transaction` should create one and pass it to your block.
7
+ def initialize(conn, opts, &blk)
8
+ @conn = conn
9
+ @opts = opts
10
+ @active = true
11
+
12
+ DeferrableGroup.new do |dg|
13
+ @dg = dg
14
+ dg.add(
15
+ conn.exec_defer("BEGIN").callback do
16
+ blk.call(self)
17
+ end.errback { |ex| rollback(ex) }
18
+ )
19
+ end.callback { commit }.errback { |ex| rollback(ex) }
20
+ end
21
+
22
+ # Signal the database to commit this transaction. You must do this
23
+ # once you've completed your queries, it won't be called automatically
24
+ # for you. Once you've committed the transaction, you cannot use it
25
+ # again; if you execute a query against a committed transaction, an
26
+ # exception will be raised.
27
+ #
28
+ def commit
29
+ if @active
30
+ @conn.exec_defer("COMMIT").callback do
31
+ @active = false
32
+ self.succeed
33
+ end.errback { |ex| rollback(ex) }
34
+ end
35
+ end
36
+
37
+ # Signal the database to abort this transaction. You only need to
38
+ # call this method if you need to rollback for some business logic
39
+ # reason -- a rollback will be automatically performed for you in the
40
+ # event of a database error or other exception.
41
+ #
42
+ def rollback(ex)
43
+ if @active
44
+ @conn.exec_defer("ROLLBACK") do
45
+ @active = false
46
+ self.fail(ex)
47
+ end
48
+ end
49
+ end
50
+
51
+ # Insert a row of data into the database table `tbl`, using the keys
52
+ # from the `params` hash as the field names, and the values from the
53
+ # `params` hash as the field data. Once the query has completed,
54
+ # `blk` will be called to allow the transaction to continue.
55
+ #
56
+ def insert(tbl, params, &blk)
57
+ exec(*insert_sql(tbl, params), &blk)
58
+ end
59
+
60
+ # Execute an arbitrary block of SQL in `sql` within the transaction.
61
+ # If you need to pass dynamic values to the query, those should be
62
+ # given in `values`, and referenced in `sql` as `$1`, `$2`, etc. The
63
+ # given block will be called if and when the query completes
64
+ # successfully.
65
+ #
66
+ def exec(sql, values=[], &blk)
67
+ unless @active
68
+ raise RuntimeError,
69
+ "Cannot execute a query in a transaction that has been closed"
70
+ end
71
+
72
+ @dg.add(
73
+ @conn.exec_defer(sql, values).
74
+ tap { |df| df.callback(&blk) if blk }
75
+ )
76
+ end
77
+ end
@@ -1,76 +1,145 @@
1
1
  require_relative './spec_helper'
2
2
 
3
3
  describe "PG::EM::Client::Helper#db_transaction" do
4
- def mock_query_chain(queries, fail_on = -1, &blk)
5
- mock_conn = double(PG::EM::Client)
4
+ let(:mock_conn) { double(PG::EM::Client) }
6
5
 
7
- queries.each_with_index do |q, i|
8
- df = EM::DefaultDeferrable.new
6
+ def expect_query_failure(q, args=nil, exec_time = 0.001)
7
+ expect_query(q, args, exec_time, :fail)
8
+ end
9
9
 
10
- ex = expect(mock_conn).
11
- to receive(:exec_defer).
12
- with(*q).
13
- and_return(df)
10
+ def expect_query(q, args=nil, exec_time = 0.001, disposition = :succeed)
11
+ df = EM::DefaultDeferrable.new
14
12
 
15
- # Rollback expects a yield
16
- if q == ["ROLLBACK"]
17
- ex.and_yield()
18
- end
19
-
20
- if i == fail_on
21
- df.fail
22
- else
23
- df.succeed
24
- end
13
+ ex = expect(mock_conn).
14
+ to receive(:exec_defer).
15
+ with(*[q, args].compact).
16
+ and_return(df).
17
+ ordered
18
+
19
+ # Rollback expects a yield
20
+ if q == "ROLLBACK"
21
+ ex.and_yield()
25
22
  end
26
23
 
27
- EM.run_block do
28
- db_transaction(mock_conn, &blk)
24
+ EM.add_timer(exec_time) do
25
+ df.__send__(disposition)
26
+ end
27
+ end
28
+
29
+ def in_transaction(&blk)
30
+ db_transaction(mock_conn, &blk).callback { EM.stop }.errback { EM.stop }
31
+ end
32
+
33
+ def in_em
34
+ EM.run do
35
+ EM.add_timer(1) { EM.stop; raise "test timeout" }
36
+ yield
29
37
  end
30
38
  end
31
39
 
32
40
  it "runs a BEGIN/COMMIT cycle by default" do
33
- mock_query_chain([["BEGIN"], ["COMMIT"]]) do
34
- EM::DefaultDeferrable.new.tap { |df| df.succeed }
41
+ in_em do
42
+ expect_query("BEGIN")
43
+ expect_query("COMMIT")
44
+ in_transaction do
45
+ # Nothing
46
+ end
35
47
  end
36
48
  end
37
-
49
+
38
50
  it "rolls back if BEGIN fails" do
39
- mock_query_chain([["BEGIN"], ["ROLLBACK"]], 0) do
40
- EM::DefaultDeferrable.new.tap { |df| df.succeed }
51
+ in_em do
52
+ expect_query_failure("BEGIN")
53
+ expect_query("ROLLBACK")
54
+ in_transaction do
55
+ # Nothing
56
+ end
41
57
  end
42
58
  end
43
-
59
+
44
60
  it "rolls back if COMMIT fails" do
45
- mock_query_chain([["BEGIN"], ["COMMIT"], ["ROLLBACK"]], 1) do
46
- EM::DefaultDeferrable.new.tap { |df| df.succeed }
61
+ in_em do
62
+ expect_query("BEGIN")
63
+ expect_query_failure("COMMIT")
64
+ expect_query("ROLLBACK")
65
+ in_transaction do
66
+ # Nothing
67
+ end
47
68
  end
48
69
  end
49
70
 
50
- it "runs a simple INSERT" do
51
- mock_query_chain([
52
- ["BEGIN"],
53
- ['INSERT INTO "foo" ("bar") VALUES ($1)',
54
- ["baz"]
55
- ],
56
- ["COMMIT"]
57
- ]) do |txn|
58
- txn.insert("foo", :bar => 'baz')
71
+ it "runs a simple INSERT correctly" do
72
+ in_em do
73
+ expect_query("BEGIN")
74
+ expect_query('INSERT INTO "foo" ("bar") VALUES ($1)', ["baz"])
75
+ expect_query("COMMIT")
76
+ in_transaction do |txn|
77
+ txn.insert("foo", :bar => 'baz')
78
+ end
59
79
  end
60
80
  end
61
81
 
62
82
  it "rolls back after a failed INSERT" do
63
- mock_query_chain(
64
- [
65
- ["BEGIN"],
66
- ['INSERT INTO "foo" ("bar") VALUES ($1)',
67
- ["baz"]
68
- ],
69
- ["ROLLBACK"]
70
- ],
71
- 1
72
- ) do |txn|
73
- txn.insert("foo", :bar => 'baz')
83
+ in_em do
84
+ expect_query("BEGIN")
85
+ expect_query_failure('INSERT INTO "foo" ("bar") VALUES ($1)', ["baz"])
86
+ expect_query("ROLLBACK")
87
+ in_transaction do |txn|
88
+ txn.insert("foo", :bar => 'baz')
89
+ end
90
+ end
91
+ end
92
+
93
+ it "runs nested inserts in the right order" do
94
+ in_em do
95
+ expect_query("BEGIN")
96
+ expect_query('INSERT INTO "foo" ("bar") VALUES ($1)', ['baz'])
97
+ expect_query('INSERT INTO "foo" ("bar") VALUES ($1)', ['wombat'])
98
+ expect_query('INSERT INTO "foo" ("bar") VALUES ($1)', ['quux'])
99
+ expect_query("COMMIT")
100
+
101
+ in_transaction do |txn|
102
+ txn.insert("foo", :bar => 'baz') do
103
+ txn.insert("foo", :bar => 'wombat') do
104
+ txn.insert("foo", :bar => 'quux')
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
110
+
111
+ it "is robust against slow queries" do
112
+ # All tests up to now *could* have just passed "by accident", because
113
+ # the queries were running fast enough to come out in order, even if
114
+ # we weren't properly synchronising. However, by making the second
115
+ # insert run slowly, we should be able to be confident that we're
116
+ # properly running the queries in order.
117
+ in_em do
118
+ expect_query("BEGIN")
119
+ expect_query('INSERT INTO "foo" ("bar") VALUES ($1)', ['baz'])
120
+ expect_query('INSERT INTO "foo" ("bar") VALUES ($1)', ['wombat'], 0.1)
121
+ expect_query('INSERT INTO "foo" ("bar") VALUES ($1)', ['quux'])
122
+ expect_query("COMMIT")
123
+
124
+ in_transaction do |txn|
125
+ txn.insert("foo", :bar => 'baz') do
126
+ txn.insert("foo", :bar => 'wombat') do
127
+ txn.insert("foo", :bar => 'quux')
128
+ end
129
+ end
130
+ end
131
+ end
132
+ end
133
+
134
+ it "doesn't COMMIT if we rolled back" do
135
+ in_em do
136
+ expect_query("BEGIN")
137
+ expect_query('INSERT INTO "foo" ("bar") VALUES ($1)', ["baz"])
138
+ expect_query("ROLLBACK")
139
+ in_transaction do |txn|
140
+ txn.insert("foo", :bar => 'baz')
141
+ txn.rollback("Because I can")
142
+ end
74
143
  end
75
144
  end
76
145
  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.2.0
4
+ version: 0.3.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: 2014-10-23 00:00:00.000000000 Z
12
+ date: 2014-10-26 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: git-version-bump
@@ -218,6 +218,8 @@ files:
218
218
  - Rakefile
219
219
  - em-pg-client-helper.gemspec
220
220
  - lib/em-pg-client-helper.rb
221
+ - lib/em-pg-client-helper/deferrable_group.rb
222
+ - lib/em-pg-client-helper/transaction.rb
221
223
  - spec/db_insert_spec.rb
222
224
  - spec/db_transaction_spec.rb
223
225
  - spec/insert_sql_spec.rb
@@ -236,7 +238,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
236
238
  version: '0'
237
239
  segments:
238
240
  - 0
239
- hash: 3974973386782984490
241
+ hash: 2182591116081055210
240
242
  required_rubygems_version: !ruby/object:Gem::Requirement
241
243
  none: false
242
244
  requirements:
@@ -245,7 +247,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
245
247
  version: '0'
246
248
  segments:
247
249
  - 0
248
- hash: 3974973386782984490
250
+ hash: 2182591116081055210
249
251
  requirements: []
250
252
  rubyforge_project:
251
253
  rubygems_version: 1.8.23