em-pg-client 0.3.2 → 0.3.3

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