em-pg-client-helper 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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