em-pg-sequel 0.0.3 → 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile CHANGED
@@ -2,8 +2,8 @@ source 'https://rubygems.org'
2
2
 
3
3
  group :test do
4
4
  gem "sequel"
5
- gem "minitest"
6
- gem "minitest-reporters"
5
+ gem "minitest", "~> 4"
6
+ gem "minitest-reporters", "< 1"
7
7
  gem "debugger"
8
8
  end
9
9
 
data/README.md CHANGED
@@ -8,9 +8,10 @@ Usage
8
8
 
9
9
  ```ruby
10
10
  require "em-pg-sequel"
11
+ require "em-synchrony"
11
12
  EM.synchrony do
12
13
  url = "postgres://postgres:postgres@localhost:5432/test"
13
- db = Sequel.connect(url, pool_class: EM::PG::ConnectionPool)
14
+ db = Sequel.connect(url, pool_class: :em_synchrony)
14
15
 
15
16
  puts db[:test].all.inspect
16
17
 
data/em-pg-sequel.gemspec CHANGED
@@ -4,8 +4,8 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
4
 
5
5
  Gem::Specification.new do |gem|
6
6
  gem.name = "em-pg-sequel"
7
- gem.version = "0.0.3"
8
- gem.authors = ["Petr Yanovich"]
7
+ gem.version = "0.0.4"
8
+ gem.authors = ["Petr Yanovich", "Rafal Michalski"]
9
9
  gem.email = ["fl00r@yandex.ru"]
10
10
  gem.description = %q{Sequel adapter for ruby-em-pg-client}
11
11
  gem.summary = %q{Sequel adapter for ruby-em-pg-client}
@@ -17,8 +17,7 @@ Gem::Specification.new do |gem|
17
17
  gem.require_paths = ["lib"]
18
18
 
19
19
  gem.add_dependency "sequel"
20
- gem.add_dependency "em-synchrony"
21
- gem.add_dependency "em-pg-client"
20
+ gem.add_dependency "em-pg-client", ">= 0.3.0"
22
21
 
23
22
  gem.add_development_dependency "em-synchrony"
24
23
  end
data/lib/em-pg-sequel.rb CHANGED
@@ -1,53 +1,10 @@
1
- require 'em-synchrony'
2
- require 'em-synchrony/pg'
1
+ require 'em-pg-client'
3
2
  require 'sequel'
4
3
  require 'em-pg-sequel/connection_pool'
5
4
 
6
- module EM::PG
7
- class ConnectionPool < ::Sequel::ConnectionPool
8
- DEFAULT_SIZE = 4
9
- attr_accessor :pool
10
- def initialize(db, opts = {})
11
- super
12
- size = opts[:max_connections] || DEFAULT_SIZE
13
- @pool = ::EM::PG::Sequel::ConnectionPool.new(size: size, disconnect_class: ::Sequel::DatabaseConnectionError) do
14
- make_new(DEFAULT_SERVER)
15
- end
16
- end
17
-
18
- def size
19
- @pool.available.size
20
- end
21
-
22
- def hold(server = nil, &blk)
23
- @pool.execute(&blk)
24
- end
25
-
26
- def disconnect(server = nil)
27
- @pool.available.each{ |conn| db.disconnect_connection(conn) }
28
- @pool.available.clear
29
- end
30
- end
31
- end
32
-
33
- module PG::EM
34
- class SyncClient < Client
35
- # Dirty hack
36
- # To avoid patching ruby-em-pg-client and to support Sequel API
37
- # we should execute async_client asynchronously for em-pg and synchronously for sequel
38
- def async_exec(*args)
39
- if block_given?
40
- super
41
- else
42
- exec(*args)
43
- end
44
- end
45
- end
46
- end
47
-
48
5
  $VERBOSE.tap do |old_verbose|
49
6
  $VERBOSE = nil
50
- PGconn = PG::EM::SyncClient
7
+ PGconn = PG::EM::Client
51
8
  $VERBOSE = old_verbose
52
9
  end
53
10
 
@@ -1,47 +1,97 @@
1
1
  module EM::PG
2
2
  module Sequel
3
- class ConnectionPool
4
- attr_reader :available
3
+ class ConnectionPool < ::Sequel::ConnectionPool
5
4
 
6
- def initialize(opts, &blk)
5
+ DEFAULT_SIZE = 4
6
+
7
+ attr_reader :available, :allocated, :max_size
8
+
9
+ def initialize(db, opts = {})
10
+ super
7
11
  @available = []
12
+ @allocated = {}
8
13
  @pending = []
9
- @acquire_blk = blk
10
14
 
11
- @disconnected_class = opts[:disconnect_class]
15
+ @max_size = opts[:max_connections] || DEFAULT_SIZE
16
+ hold {}
17
+ end
18
+
19
+ def size
20
+ @available.length + @allocated.length
21
+ end
22
+
23
+ def hold(server = nil)
24
+ fiber = Fiber.current
25
+ fiber_id = fiber.object_id
26
+
27
+ if conn = @allocated[fiber_id]
28
+ skip_release = true
29
+ else
30
+ conn = acquire(fiber) until conn
31
+ end
32
+
33
+ begin
34
+ yield conn
35
+
36
+ rescue ::Sequel::DatabaseDisconnectError => e
37
+ db.disconnect_connection(conn)
38
+ drop_failed(fiber_id)
39
+ skip_release = true
12
40
 
13
- opts[:size].times do
14
- @available.push @acquire_blk.call
41
+ raise
42
+ ensure
43
+ release(fiber_id) unless skip_release
15
44
  end
16
45
  end
17
46
 
18
- def execute
19
- conn = acquire
20
- yield conn
21
- rescue => e
22
- conn = @acquire_blk.call if @disconnected_class && @disconnected_class === e
23
- raise
24
- ensure
25
- release(conn)
47
+ def disconnect(server = nil)
48
+ @available.each{ |conn| db.disconnect_connection(conn) }
49
+ @available.clear
26
50
  end
27
51
 
28
- def acquire
29
- f = Fiber.current
52
+ private
53
+
54
+ def acquire(fiber)
30
55
  if conn = @available.pop
31
- conn
56
+ @allocated[fiber.object_id] = conn
32
57
  else
33
- @pending << f
34
- Fiber.yield
58
+ if size < max_size
59
+ allocate_new_connection(fiber.object_id)
60
+ else
61
+ @pending << fiber
62
+ Fiber.yield
63
+ end
35
64
  end
36
65
  end
37
66
 
38
- def release(conn)
39
- if job = @pending.shift
40
- EM.next_tick{ job.resume conn }
67
+ def allocate_new_connection(fiber_id)
68
+ @allocated[fiber_id] = true
69
+ @allocated[fiber_id] = make_new(DEFAULT_SERVER)
70
+ rescue Exception => e
71
+ drop_failed(fiber_id)
72
+ raise e
73
+ end
74
+
75
+ # drop failed connection (or a mark) from the pool and
76
+ # ensure that the pending requests won't starve
77
+ def drop_failed(fiber_id)
78
+ @allocated.delete(fiber_id)
79
+ if pending = @pending.shift
80
+ EM.next_tick { pending.resume }
81
+ end
82
+ end
83
+
84
+ def release(fiber_id)
85
+ conn = @allocated.delete(fiber_id)
86
+ if pending = @pending.shift
87
+ @allocated[pending.object_id] = conn
88
+ EM.next_tick { pending.resume conn}
41
89
  else
42
90
  @available << conn
43
91
  end
44
92
  end
93
+
94
+ CONNECTION_POOL_MAP[:em_synchrony] = self
45
95
  end
46
96
  end
47
97
  end
@@ -3,20 +3,44 @@ require 'em-synchrony'
3
3
  require 'em-synchrony/fiber_iterator'
4
4
 
5
5
  describe EM::PG::Sequel do
6
+ include SynchronyUtils
7
+
6
8
  DELAY = 1
7
9
  QUERY = "select pg_sleep(#{DELAY})"
8
10
 
9
11
  let(:url) { DB_URL }
10
12
  let(:size) { 1 }
11
- let(:db) { Sequel.connect(url, max_connection: size, pool_class: EM::PG::ConnectionPool, db_logger: Logger.new(nil)) }
13
+ let(:db) { Sequel.connect(url, max_connections: size, pool_class: :em_synchrony, db_logger: Logger.new(nil)) }
12
14
  let(:test) { db[:test] }
15
+ let(:fiber_iterator) { EM::Synchrony::FiberIterator }
16
+
17
+ describe "sanity" do
18
+ let(:size) { 42 }
19
+
20
+ it "should have max_size 42" do
21
+ db.pool.max_size.must_equal 42
22
+ end
23
+
24
+ it "should not release nil connection on connect error" do
25
+ synchrony do
26
+ db.disconnect
27
+ db.pool.size.must_equal 0
28
+ db.pool.stub :allocate_new_connection,
29
+ proc { raise Sequel::DatabaseConnectionError } do
30
+
31
+ proc { test.count }.must_raise Sequel::DatabaseConnectionError
32
+ db.pool.size.must_equal 0
33
+ end
34
+
35
+ end
36
+ end
37
+ end
13
38
 
14
39
  describe "unexist table" do
15
40
  it "should raise exception" do
16
- EM.synchrony do
41
+ synchrony do
17
42
  proc { test.all }.must_raise Sequel::DatabaseError
18
43
 
19
- EM.stop
20
44
  end
21
45
  end
22
46
  end
@@ -24,30 +48,27 @@ describe EM::PG::Sequel do
24
48
  describe "exist table" do
25
49
 
26
50
  before do
27
- EM.synchrony do
51
+ synchrony do
28
52
  db.create_table!(:test) do
29
53
  text :name
30
54
  integer :value, index: true
31
55
  end
32
56
 
33
- EM.stop
34
57
  end
35
58
  end
36
59
 
37
60
  after do
38
- EM.synchrony do
61
+ synchrony do
39
62
  db.drop_table?(:test)
40
63
 
41
- EM.stop
42
64
  end
43
65
  end
44
66
 
45
67
  it "should connect and execute query" do
46
- EM.synchrony do
68
+ synchrony do
47
69
  test.insert name: "andrew", value: 42
48
70
  test.where(name: "andrew").first[:value].must_equal 42
49
71
 
50
- EM.stop
51
72
  end
52
73
  end
53
74
 
@@ -55,17 +76,16 @@ describe EM::PG::Sequel do
55
76
  describe "pool size is exceeded" do
56
77
  let(:size) { 1 }
57
78
  it "should queue requests" do
58
- EM.synchrony do
79
+ synchrony do
59
80
  start = Time.now.to_f
60
81
 
61
82
  res = []
62
- EM::Synchrony::FiberIterator.new([1,2], 1).each do |t|
83
+ fiber_iterator.new([1,2], 1).each do |t|
63
84
  res << db[QUERY].all
64
85
  end
65
86
  (Time.now.to_f - start.to_f).must_be_within_delta DELAY * 2, DELAY * 2 * 0.15
66
87
  res.size.must_equal 2
67
88
 
68
- EM.stop
69
89
  end
70
90
  end
71
91
  end
@@ -73,18 +93,171 @@ describe EM::PG::Sequel do
73
93
  describe "pool size is enough" do
74
94
  let(:size) { 2 }
75
95
  it "should parallel requests" do
76
- EM.synchrony do
96
+ synchrony do
77
97
  start = Time.now.to_f
78
98
 
79
99
  res = []
80
- EM::Synchrony::FiberIterator.new([1,2], 2).each do |t|
100
+ fiber_iterator.new([1,2], 2).each do |t|
81
101
  res << db[QUERY].all
82
102
  end
83
103
 
84
104
  (Time.now.to_f - start.to_f).must_be_within_delta DELAY, DELAY * 0.30
85
105
  res.size.must_equal 2
86
106
 
87
- EM.stop
107
+ end
108
+ end
109
+ end
110
+
111
+ describe "pool size is dynamic" do
112
+
113
+ let(:size) { 2 }
114
+
115
+ it "should have initial size of one" do
116
+ db.pool.size.must_equal 1
117
+ end
118
+
119
+ it "should allocate second connection" do
120
+ synchrony do
121
+ res = []
122
+ res << test.first
123
+ db.pool.size.must_equal 1
124
+ fiber_iterator.new([1,2], 2).each do |t|
125
+ res << db[QUERY].all
126
+ end
127
+ db.pool.size.must_equal 2
128
+ res.size.must_equal 3
129
+
130
+ end
131
+ end
132
+
133
+ it "should not create more than size connections" do
134
+ synchrony do
135
+ db.pool.size.must_equal 1
136
+
137
+ start = Time.now.to_f
138
+ res = []
139
+ fiber_iterator.new([1,1,2], 3).each do |pool_size|
140
+ db.pool.size.must_equal pool_size
141
+ res << db[QUERY].all
142
+ end
143
+
144
+ (Time.now.to_f - start.to_f).must_be_within_delta DELAY*2, DELAY * 2.60
145
+ res.size.must_equal 3
146
+
147
+ db.pool.size.must_equal size
148
+
149
+ end
150
+ end
151
+
152
+ it "should clear all connections on disconnect" do
153
+ synchrony do
154
+ db.disconnect
155
+ db.pool.size.must_equal 0
156
+ res = []
157
+ fiber_iterator.new([1,2,3], 3).each do |t|
158
+ res << test.count
159
+ end
160
+ res.size.must_equal 3
161
+ db.pool.size.must_equal size
162
+ db.disconnect
163
+ db.pool.size.must_equal 0
164
+
165
+ end
166
+ end
167
+
168
+ it "should re-create 1st connection" do
169
+ synchrony do
170
+ db.disconnect
171
+ db.pool.size.must_equal 0
172
+
173
+ test.count.must_equal 0
174
+ db.pool.size.must_equal 1
175
+
176
+ end
177
+ end
178
+
179
+ end
180
+
181
+ describe "on connection errors" do
182
+
183
+ let(:size) { 3 }
184
+
185
+ it "should not leave pending requests in queue" do
186
+ synchrony do
187
+ db.disconnect
188
+ fiber_iterator.new((0..size), size).each { test.count }
189
+ db.pool.available.each do |conn|
190
+ # force clients to disconnected state
191
+ conn.async_command_aborted = true
192
+ end.length.must_equal db.pool.size
193
+
194
+ db.pool.stub :make_new,
195
+ proc {
196
+ EM::Synchrony.sleep 0.1
197
+ raise Sequel::DatabaseConnectionError } do
198
+
199
+ request_counter = 0
200
+ expected_runs = db.pool.max_size + 10
201
+ expected_runs.times do |index|
202
+ Fiber.new do
203
+
204
+ pending = db.pool.instance_eval { @pending.length }
205
+ pending.must_equal [index - db.pool.max_size, 0].max
206
+
207
+ if index < db.pool.max_size
208
+ proc { test.count }.must_raise Sequel::DatabaseDisconnectError
209
+ else
210
+ proc { test.count }.must_raise Sequel::DatabaseConnectionError
211
+ end
212
+
213
+ request_counter += 1
214
+
215
+ end.resume
216
+ end
217
+
218
+ db.pool.instance_eval { @pending.length }.must_equal 10
219
+
220
+ tick_sleep while request_counter < expected_runs
221
+
222
+ db.pool.instance_eval { @pending.length }.must_equal 0
223
+ db.pool.size.must_equal 0
224
+ request_counter.must_equal expected_runs
225
+ end
226
+
227
+ end
228
+ end
229
+ end
230
+
231
+ describe "play nice with transactions" do
232
+
233
+ let(:size) { 2 }
234
+
235
+ it "should lock connection to fiber" do
236
+ synchrony do
237
+ db.transaction do |conn|
238
+ db.in_transaction?.must_equal true
239
+ db.transaction do |inner_conn|
240
+ inner_conn.must_be_same_as conn
241
+ db.in_transaction?.must_equal true
242
+ end
243
+ end
244
+
245
+ end
246
+ end
247
+
248
+ it "should allow separate transactions" do
249
+ synchrony do
250
+ db.transaction do |conn|
251
+ db.in_transaction?.must_equal true
252
+ fiber_iterator.new([1,2], 2).each do |t|
253
+ db.in_transaction?.must_equal false
254
+ db.transaction do |inner_conn|
255
+ inner_conn.wont_be_same_as conn
256
+ db.in_transaction?.must_equal true
257
+ end
258
+ end
259
+ end
260
+
88
261
  end
89
262
  end
90
263
  end
data/spec/spec_helper.rb CHANGED
@@ -16,6 +16,24 @@ DB_CONFIG = {
16
16
  password: "postgres",
17
17
  }
18
18
 
19
+ module SynchronyUtils
20
+ def tick_sleep
21
+ f = Fiber.current
22
+ EM::next_tick { f.resume }
23
+ Fiber.yield
24
+ end
25
+
26
+ def synchrony
27
+ EM.synchrony do
28
+ begin
29
+ yield
30
+ ensure
31
+ EM.stop
32
+ end
33
+ end
34
+ end
35
+ end
36
+
19
37
  DB_URL = "postgres://%s:%s@%s:%d/%s" % [DB_CONFIG[:user], DB_CONFIG[:password], DB_CONFIG[:host], DB_CONFIG[:port], DB_CONFIG[:dbname]]
20
38
 
21
39
  MiniTest::Reporters.use! MiniTest::Reporters::SpecReporter.new
metadata CHANGED
@@ -1,80 +1,65 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: em-pg-sequel
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.3
4
+ version: 0.0.4
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
8
8
  - Petr Yanovich
9
+ - Rafal Michalski
9
10
  autorequire:
10
11
  bindir: bin
11
12
  cert_chain: []
12
- date: 2013-02-25 00:00:00.000000000 Z
13
+ date: 2014-01-03 00:00:00.000000000 Z
13
14
  dependencies:
14
15
  - !ruby/object:Gem::Dependency
15
16
  name: sequel
16
- requirement: !ruby/object:Gem::Requirement
17
- none: false
18
- requirements:
19
- - - ! '>='
20
- - !ruby/object:Gem::Version
21
- version: '0'
22
- type: :runtime
23
17
  prerelease: false
24
- version_requirements: !ruby/object:Gem::Requirement
25
- none: false
26
- requirements:
27
- - - ! '>='
28
- - !ruby/object:Gem::Version
29
- version: '0'
30
- - !ruby/object:Gem::Dependency
31
- name: em-synchrony
32
18
  requirement: !ruby/object:Gem::Requirement
33
- none: false
34
19
  requirements:
35
20
  - - ! '>='
36
21
  - !ruby/object:Gem::Version
37
22
  version: '0'
23
+ none: false
38
24
  type: :runtime
39
- prerelease: false
40
25
  version_requirements: !ruby/object:Gem::Requirement
41
- none: false
42
26
  requirements:
43
27
  - - ! '>='
44
28
  - !ruby/object:Gem::Version
45
29
  version: '0'
30
+ none: false
46
31
  - !ruby/object:Gem::Dependency
47
32
  name: em-pg-client
33
+ prerelease: false
48
34
  requirement: !ruby/object:Gem::Requirement
49
- none: false
50
35
  requirements:
51
36
  - - ! '>='
52
37
  - !ruby/object:Gem::Version
53
- version: '0'
38
+ version: 0.3.0
39
+ none: false
54
40
  type: :runtime
55
- prerelease: false
56
41
  version_requirements: !ruby/object:Gem::Requirement
57
- none: false
58
42
  requirements:
59
43
  - - ! '>='
60
44
  - !ruby/object:Gem::Version
61
- version: '0'
45
+ version: 0.3.0
46
+ none: false
62
47
  - !ruby/object:Gem::Dependency
63
48
  name: em-synchrony
49
+ prerelease: false
64
50
  requirement: !ruby/object:Gem::Requirement
65
- none: false
66
51
  requirements:
67
52
  - - ! '>='
68
53
  - !ruby/object:Gem::Version
69
54
  version: '0'
55
+ none: false
70
56
  type: :development
71
- prerelease: false
72
57
  version_requirements: !ruby/object:Gem::Requirement
73
- none: false
74
58
  requirements:
75
59
  - - ! '>='
76
60
  - !ruby/object:Gem::Version
77
61
  version: '0'
62
+ none: false
78
63
  description: Sequel adapter for ruby-em-pg-client
79
64
  email:
80
65
  - fl00r@yandex.ru
@@ -99,17 +84,17 @@ rdoc_options: []
99
84
  require_paths:
100
85
  - lib
101
86
  required_ruby_version: !ruby/object:Gem::Requirement
102
- none: false
103
87
  requirements:
104
88
  - - ! '>='
105
89
  - !ruby/object:Gem::Version
106
90
  version: '0'
107
- required_rubygems_version: !ruby/object:Gem::Requirement
108
91
  none: false
92
+ required_rubygems_version: !ruby/object:Gem::Requirement
109
93
  requirements:
110
94
  - - ! '>='
111
95
  - !ruby/object:Gem::Version
112
96
  version: '0'
97
+ none: false
113
98
  requirements: []
114
99
  rubyforge_project:
115
100
  rubygems_version: 1.8.24