em-pg-sequel 0.0.3 → 0.0.4

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/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