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.
- data/lib/em-pg-client-helper.rb +13 -83
- data/lib/em-pg-client-helper/deferrable_group.rb +43 -0
- data/lib/em-pg-client-helper/transaction.rb +77 -0
- data/spec/db_transaction_spec.rb +117 -48
- metadata +6 -4
data/lib/em-pg-client-helper.rb
CHANGED
@@ -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.
|
52
|
-
# `
|
53
|
-
#
|
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
|
-
#
|
56
|
-
# to specify what to run after the transaction
|
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.
|
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
|
-
|
103
|
-
|
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
|
data/spec/db_transaction_spec.rb
CHANGED
@@ -1,76 +1,145 @@
|
|
1
1
|
require_relative './spec_helper'
|
2
2
|
|
3
3
|
describe "PG::EM::Client::Helper#db_transaction" do
|
4
|
-
|
5
|
-
mock_conn = double(PG::EM::Client)
|
4
|
+
let(:mock_conn) { double(PG::EM::Client) }
|
6
5
|
|
7
|
-
|
8
|
-
|
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
|
-
|
11
|
-
|
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
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
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.
|
28
|
-
|
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
|
-
|
34
|
-
|
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
|
-
|
40
|
-
|
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
|
-
|
46
|
-
|
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
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
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
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
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.
|
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-
|
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:
|
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:
|
250
|
+
hash: 2182591116081055210
|
249
251
|
requirements: []
|
250
252
|
rubyforge_project:
|
251
253
|
rubygems_version: 1.8.23
|