em-pg-client 0.3.2 → 0.3.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -78,14 +78,30 @@ module PG
78
78
  # - +:lazy+ = false - should lazy allocate first connection
79
79
  # - +:connection_class+ = {PG::EM::Client}
80
80
  #
81
+ # For convenience the given block will be set as the +on_connect+ option.
82
+ #
83
+ # @yieldparam pg [Client] connected client instance on each newly
84
+ # created connection
85
+ # @yieldparam is_async [Boolean] always +true+ in a connection pool
86
+ # context
87
+ # @yieldparam is_reset [Boolean] always +false+ unless
88
+ # +async_autoreconnect+ options is +true+ and
89
+ # was actually re-connecting
90
+ #
81
91
  # @raise [PG::Error]
82
92
  # @raise [ArgumentError]
83
- def initialize(options = {})
93
+ # @see Client#on_connect
94
+ def initialize(options = {}, &on_connect)
84
95
  @available = []
85
96
  @pending = []
86
97
  @allocated = {}
98
+ @max_size = DEFAULT_SIZE
87
99
  @connection_class = Client
88
100
 
101
+ if block_given?
102
+ options = {on_connect: on_connect}.merge(options)
103
+ end
104
+
89
105
  lazy = false
90
106
  @options = options.reject do |key, value|
91
107
  case key.to_sym
@@ -101,9 +117,7 @@ module PG
101
117
  end
102
118
  end
103
119
 
104
- @max_size ||= DEFAULT_SIZE
105
-
106
- raise ArgumentError, "#{self.class}.new: pool size must be > 1" if @max_size < 1
120
+ raise ArgumentError, "#{self.class}.new: pool size must be >= 1" if @max_size < 1
107
121
 
108
122
  # allocate first connection, unless we are lazy
109
123
  hold unless lazy
@@ -179,6 +193,10 @@ module PG
179
193
  # @return [Boolean] asynchronous auto re-connect status
180
194
  # Set {Client#async_autoreconnect} on all present and future connections
181
195
  # in this pool or read value from options
196
+ # @!attribute [rw] on_connect
197
+ # @return [Proc<Client,is_async,is_reset>] connect hook
198
+ # Set {Client#on_connect} on all present and future connections
199
+ # in this pool or read value from options
182
200
  # @!attribute [rw] on_autoreconnect
183
201
  # @return [Proc<Client, Error>] auto re-connect hook
184
202
  # Set {Client#on_autoreconnect} on all present and future connections
@@ -186,6 +204,7 @@ module PG
186
204
  %w[connect_timeout
187
205
  query_timeout
188
206
  async_autoreconnect
207
+ on_connect
189
208
  on_autoreconnect].each do |name|
190
209
  class_eval <<-EOD, __FILE__, __LINE__
191
210
  def #{name}=(value)
@@ -194,11 +213,24 @@ module PG
194
213
  @available.each(&b)
195
214
  @allocated.each_value(&b)
196
215
  end
197
-
198
- def #{name}
199
- @options[:#{name}] || @options['#{name}']
200
- end
201
216
  EOD
217
+ if name.start_with?('on_')
218
+ class_eval <<-EOD, __FILE__, __LINE__
219
+ def #{name}(&hook)
220
+ if block_given?
221
+ self.#{name} = hook
222
+ else
223
+ @options[:#{name}] || @options['#{name}']
224
+ end
225
+ end
226
+ EOD
227
+ else
228
+ class_eval <<-EOD, __FILE__, __LINE__
229
+ def #{name}
230
+ @options[:#{name}] || @options['#{name}']
231
+ end
232
+ EOD
233
+ end
202
234
  DeferredOptions.class_eval <<-EOD, __FILE__, __LINE__
203
235
  def #{name}=(value)
204
236
  self[:#{name}=] = value
@@ -327,7 +359,7 @@ module PG
327
359
  begin
328
360
  id = fiber.object_id
329
361
  # mark allocated pool for proper #size value
330
- # the connecting process will yield from fiber
362
+ # the connection is made asynchronously
331
363
  @allocated[id] = opts = DeferredOptions.new
332
364
  conn = @connection_class.new(@options)
333
365
  ensure
@@ -285,6 +285,51 @@ describe 'pg-em autoreconnect with on_autoreconnect' do
285
285
  end
286
286
  end
287
287
 
288
+ it "should execute on_connect before on_autoreconnect after server restart" do
289
+ @client.on_connect.should be_nil
290
+ run_on_connect = false
291
+ @client.on_connect = proc do |client, is_async, is_reset|
292
+ client.should be_an_instance_of PG::EM::Client
293
+ is_async.should be_true
294
+ is_reset.should be_true
295
+ client.query_defer('SELECT pg_database_size(current_database());').callback {
296
+ run_on_connect = true
297
+ }
298
+ end
299
+ @client.on_autoreconnect = proc do |client, ex|
300
+ run_on_connect.should be_true
301
+ @on_autoreconnect.call(client, ex)
302
+ end
303
+ system($pgserver_cmd_stop).should be_true
304
+ system($pgserver_cmd_start).should be_true
305
+ @tested_proc.call
306
+ end
307
+
308
+ it "should skip on_autoreconnect when on_connect failed after server restart" do
309
+ run_on_connect = false
310
+ run_on_autoreconnect = false
311
+ @client.on_connect = proc do |client, is_async, is_reset|
312
+ client.should be_an_instance_of PG::EM::Client
313
+ is_async.should be_true
314
+ is_reset.should be_true
315
+ client.query_defer('SELLECT 1;').errback {
316
+ run_on_connect = true
317
+ }
318
+ end
319
+ @client.on_autoreconnect = proc do |client, ex|
320
+ run_on_autoreconnect = true
321
+ end
322
+ system($pgserver_cmd_stop).should be_true
323
+ system($pgserver_cmd_start).should be_true
324
+ @client.exec_prepared_defer('get_db_size') do |ex|
325
+ ex.should be_an_instance_of PG::SyntaxError
326
+ @client.status.should be PG::CONNECTION_OK
327
+ run_on_connect.should be_true
328
+ run_on_autoreconnect.should be_false
329
+ EM.stop
330
+ end.should be_a_kind_of ::EM::DefaultDeferrable
331
+ end
332
+
288
333
  before(:all) do
289
334
  @tested_proc = proc do
290
335
  @client.exec_prepared_defer('get_db_size') do |result|
@@ -0,0 +1,171 @@
1
+ $:.unshift "lib"
2
+ gem 'eventmachine', '~> 1.0.0'
3
+ gem 'pg', ENV['EM_PG_CLIENT_TEST_PG_VERSION']
4
+ require 'eventmachine'
5
+ require 'em-synchrony'
6
+ require 'pg/em'
7
+ module PGSpecMacros
8
+ def test_blocking
9
+ client = subject.new(options)
10
+ client.should be_an_instance_of subject
11
+ client.on_connect.should be on_connect
12
+ client.exec_prepared(query_name) do |result|
13
+ result.should be_an_instance_of PG::Result
14
+ result[0]['pg_database_size'].to_i.should be > 0
15
+ end
16
+ client.reset
17
+ client.exec_prepared(query_name) do |result|
18
+ result.should be_an_instance_of PG::Result
19
+ result[0]['pg_database_size'].to_i.should be > 0
20
+ end
21
+ end
22
+
23
+ def test_blocking_error
24
+ expect do
25
+ subject.new(options)
26
+ end.to raise_error(on_connect_exception)
27
+ client = subject.new
28
+ client.should be_an_instance_of subject
29
+ client.on_connect = on_connect
30
+ expect do
31
+ client.reset
32
+ end.to raise_error(on_connect_exception)
33
+ end
34
+ end
35
+
36
+ RSpec.configure do |config|
37
+ config.include(PGSpecMacros)
38
+ end
39
+
40
+ shared_context 'test on_connect' do
41
+
42
+ it "should invoke on_connect after deferrable connect and reset" do
43
+ EM.run do
44
+ subject.connect_defer(options) do |client|
45
+ client.should be_an_instance_of subject
46
+ client.on_connect.should be on_connect
47
+ client.exec_prepared_defer(query_name) do |result|
48
+ result.should be_an_instance_of PG::Result
49
+ result[0]['pg_database_size'].to_i.should be > 0
50
+ client.reset_defer do |client|
51
+ client.should be_an_instance_of subject
52
+ client.exec_prepared_defer(query_name) do |result|
53
+ result.should be_an_instance_of PG::Result
54
+ result[0]['pg_database_size'].to_i.should be > 0
55
+ end.should be_a_kind_of ::EM::DefaultDeferrable
56
+ EM.stop
57
+ end.should be_a_kind_of ::EM::DefaultDeferrable
58
+ end.should be_a_kind_of ::EM::DefaultDeferrable
59
+ end.should be_a_kind_of ::EM::DefaultDeferrable
60
+ end
61
+ end
62
+
63
+ it "should invoke on_connect after synchrony connect and reset" do
64
+ EM.synchrony do
65
+ test_blocking
66
+ EM.stop
67
+ end
68
+ end
69
+
70
+ end
71
+
72
+ shared_context 'test on_connect error' do
73
+
74
+ it "should fail on_connect with exception after deferrable connect and reset" do
75
+ EM.run do
76
+ subject.connect_defer(options) do |ex|
77
+ ex.should be_an_instance_of on_connect_exception
78
+ subject.connect_defer do |client|
79
+ client.should be_an_instance_of subject
80
+ client.on_connect = on_connect
81
+ client.reset_defer do |ex|
82
+ ex.should be_an_instance_of on_connect_exception
83
+ EM.stop
84
+ end.should be_a_kind_of ::EM::DefaultDeferrable
85
+ end.should be_a_kind_of ::EM::DefaultDeferrable
86
+ end.should be_a_kind_of ::EM::DefaultDeferrable
87
+ end
88
+ end
89
+
90
+ it "should fail on_connect with exception after synchrony connect and reset" do
91
+ EM.synchrony do
92
+ test_blocking_error
93
+ EM.stop
94
+ end
95
+ end
96
+ end
97
+
98
+ shared_context 'test blocking' do
99
+ it "should invoke on_connect after blocking connect and reset" do
100
+ test_blocking
101
+ end
102
+ end
103
+
104
+ shared_context 'test blocking on_connect error' do
105
+ it "should fail on_connect with exception after blocking connect and reset" do
106
+ test_blocking_error
107
+ end
108
+ end
109
+
110
+ describe 'on_connect option' do
111
+ subject { PG::EM::Client }
112
+ let(:query_name) { 'get_db_size' }
113
+ let(:query) { 'SELECT pg_database_size(current_database());' }
114
+ let(:options) { {on_connect: on_connect} }
115
+ let(:sleep_query){ 'SELECT pg_sleep(0.1)'}
116
+ let(:on_connect_exception) { Class.new(StandardError) }
117
+
118
+
119
+ describe 'with deferrable on_connect' do
120
+ let(:on_connect) { proc {|client, is_async|
121
+ is_async.should be_true
122
+ PG::EM::FeaturedDeferrable.new.tap do |df|
123
+ client.exec_defer(sleep_query).callback do
124
+ df.bind_status client.prepare_defer(query_name, query)
125
+ end.errback { df.fail on_connect_exception }
126
+ end
127
+ } }
128
+
129
+ include_context 'test on_connect'
130
+ end
131
+
132
+ describe 'with synchrony on_connect' do
133
+ let(:on_connect) { proc {|client, is_async|
134
+ is_async.should be_true
135
+ was_async = false
136
+ EM.next_tick { was_async = true }
137
+ client.exec(sleep_query)
138
+ client.prepare(query_name, query)
139
+ was_async.should be_true
140
+ } }
141
+
142
+ include_context 'test on_connect'
143
+ end
144
+
145
+ describe 'with blocking on_connect' do
146
+ let(:on_connect) { proc {|client, is_async|
147
+ is_async.should be_false
148
+ client.prepare(query_name, query)
149
+ } }
150
+
151
+ include_context 'test blocking'
152
+ end
153
+
154
+ describe 'with error raised in on_connect' do
155
+ let(:on_connect) { proc {|client|
156
+ raise on_connect_exception
157
+ } }
158
+
159
+ include_context 'test on_connect error'
160
+ include_context 'test blocking on_connect error'
161
+ end
162
+
163
+ describe 'with on_connect deferrable failure' do
164
+ let(:on_connect) { proc {|client|
165
+ EM::DefaultDeferrable.new.tap {|df| df.fail on_connect_exception.new }
166
+ } }
167
+
168
+ include_context 'test on_connect error'
169
+ end
170
+
171
+ end
@@ -0,0 +1,198 @@
1
+ $:.unshift "lib"
2
+ gem 'eventmachine', '~> 1.0.0'
3
+ gem 'pg', ENV['EM_PG_CLIENT_TEST_PG_VERSION']
4
+ require 'eventmachine'
5
+ require 'em-synchrony'
6
+ require 'em-synchrony/fiber_iterator'
7
+ require 'pg/em/connection_pool'
8
+
9
+ shared_context 'test on_connect' do
10
+
11
+ it 'should call prepared statement concurrently with synchrony' do
12
+ results = []
13
+ pool = subject.new(options)
14
+ pool.max_size.should eq concurrency
15
+ pool.size.should eq 1
16
+ start = Time.now
17
+ EM::Synchrony::FiberIterator.new((1..concurrency), concurrency).each do |index|
18
+ pool.exec_prepared(query_name) do |result|
19
+ result.should be_an_instance_of PG::Result
20
+ result[0]['pg_database_size'].to_i.should be > 0
21
+ end
22
+ pool.query(sleep_query).should be_an_instance_of PG::Result
23
+ results << index
24
+ end
25
+ delta = Time.now - start
26
+ delta.should be_between(sleep_interval, sleep_interval * concurrency / 2)
27
+ results.sort.should eq (1..concurrency).to_a
28
+ pool.size.should eq concurrency
29
+ EM.stop
30
+ end
31
+
32
+ it 'should call prepared statement concurrently with deferrable' do
33
+ results = []
34
+ subject.connect_defer(options) do |pool|
35
+ pool.max_size.should eq concurrency
36
+ pool.size.should eq 1
37
+ start = Time.now
38
+ concurrency.times do |index|
39
+ pool.exec_prepared_defer(query_name) do |result|
40
+ result.should be_an_instance_of PG::Result
41
+ result[0]['pg_database_size'].to_i.should be > 0
42
+ pool.query_defer(sleep_query) do |result|
43
+ result.should be_an_instance_of PG::Result
44
+ results << index
45
+ if results.length == concurrency
46
+ delta = Time.now - start
47
+ delta.should be_between(sleep_interval, sleep_interval * concurrency / 2)
48
+ results.sort.should eq (0...concurrency).to_a
49
+ pool.size.should eq concurrency
50
+ EM.stop
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+
59
+ shared_context 'test on_connect error' do
60
+
61
+ it "should fail on_connect with exception after synchrony connect" do
62
+ expect do
63
+ subject.new(options)
64
+ end.to raise_error(on_connect_exception)
65
+ client = subject.new(options.merge(lazy: true))
66
+ client.should be_an_instance_of subject
67
+ expect do
68
+ client.query(sleep_query)
69
+ end.to raise_error(on_connect_exception)
70
+ EM.stop
71
+ end
72
+
73
+ it "should fail on_connect with exception after deferrable connect" do
74
+ subject.connect_defer(options) do |ex|
75
+ ex.should be_an_instance_of on_connect_exception
76
+ pool = subject.new(options.merge(lazy: true))
77
+ pool.should be_an_instance_of subject
78
+ pool.query_defer(sleep_query) do |ex|
79
+ ex.should be_an_instance_of on_connect_exception
80
+ EM.stop
81
+ end.should be_an_instance_of PG::EM::FeaturedDeferrable
82
+ end.should be_an_instance_of PG::EM::FeaturedDeferrable
83
+ end
84
+
85
+ end
86
+
87
+ describe 'connection pool' do
88
+ subject { PG::EM::ConnectionPool }
89
+
90
+ describe 'on_connect' do
91
+ let(:query_name) { 'get_db_size' }
92
+ let(:query) { 'SELECT pg_database_size(current_database());' }
93
+ let(:concurrency) { 10 }
94
+ let(:options) { {size: concurrency, on_connect: on_connect} }
95
+ let(:sleep_interval) { 0.1 }
96
+ let(:sleep_query) { "SELECT pg_sleep(#{sleep_interval})"}
97
+ let(:on_connect_exception) { Class.new(StandardError) }
98
+ let(:on_connect) { proc {} }
99
+
100
+ around(:each) do |testcase|
101
+ EM.synchrony do
102
+ begin
103
+ testcase.call
104
+ end
105
+ end
106
+ end
107
+
108
+ it 'should setup block as on_connect client option' do
109
+ connect_hook = false
110
+ pool = subject.new { connect_hook = true }
111
+ connect_hook.should be_true
112
+ pool.should be_an_instance_of subject
113
+ pool.on_connect.should be_an_instance_of Proc
114
+ EM.stop
115
+ end
116
+
117
+ it 'should prefer on_connect from options' do
118
+ connect_hook = false
119
+ pool = subject.new(options) { connect_hook = true }
120
+ connect_hook.should be_false
121
+ pool.should be_an_instance_of subject
122
+ pool.on_connect.should be on_connect
123
+ EM.stop
124
+ end
125
+
126
+ describe 'with deferrable on_connect' do
127
+ let(:on_connect) { proc {|client, is_async|
128
+ is_async.should be_true
129
+ PG::EM::FeaturedDeferrable.new.tap do |df|
130
+ client.exec_defer(sleep_query).callback do
131
+ df.bind_status client.prepare_defer(query_name, query)
132
+ end.errback { df.fail on_connect_exception }
133
+ end
134
+ } }
135
+
136
+ include_context 'test on_connect'
137
+ end
138
+
139
+ describe 'with synchrony on_connect' do
140
+ let(:on_connect) { proc {|client, is_async|
141
+ is_async.should be_true
142
+ was_async = false
143
+ EM.next_tick { was_async = true }
144
+ client.exec(sleep_query)
145
+ client.prepare(query_name, query)
146
+ was_async.should be_true
147
+ } }
148
+
149
+ include_context 'test on_connect'
150
+ end
151
+
152
+ describe 'with error raised in on_connect' do
153
+ let(:on_connect) { proc {|client|
154
+ raise on_connect_exception
155
+ } }
156
+
157
+ include_context 'test on_connect error'
158
+ end
159
+
160
+ describe 'with on_connect deferrable failure' do
161
+ let(:on_connect) { proc {|client|
162
+ EM::DefaultDeferrable.new.tap {|df| df.fail on_connect_exception.new }
163
+ } }
164
+
165
+ include_context 'test on_connect error'
166
+ end
167
+ end
168
+
169
+ describe '#transaction' do
170
+ let(:concurrency) { 2 }
171
+ let(:options) { {size: concurrency} }
172
+ let(:query) { 'SELECT pg_database_size(current_database());' }
173
+
174
+ around(:each) do |testcase|
175
+ EM.synchrony do
176
+ begin
177
+ @pool = subject.new(options)
178
+ testcase.call
179
+ end
180
+ end
181
+ end
182
+
183
+ it 'should lock transaction connection to fiber' do
184
+ @pool.transaction do |pg|
185
+ @pool.hold {|c| c.should be pg }
186
+ Fiber.new do
187
+ @pool.size.should eq 1
188
+ @pool.hold {|c| c.should_not be pg }
189
+ @pool.size.should eq 2
190
+ EM.stop
191
+ end.resume
192
+ @pool.hold {|c| c.should be pg }
193
+ @pool.size.should eq 2
194
+ end
195
+ end
196
+
197
+ end
198
+ end