superfeedr-em-mysql 0.4.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.
Files changed (6) hide show
  1. data/README +40 -0
  2. data/em-mysql.gemspec +21 -0
  3. data/lib/em/mysql.rb +306 -0
  4. data/lib/sequel/async.rb +184 -0
  5. data/test.rb +59 -0
  6. metadata +68 -0
data/README ADDED
@@ -0,0 +1,40 @@
1
+ Async MySQL driver for Ruby/EventMachine
2
+ (c) 2008 Aman Gupta (tmm1)
3
+
4
+
5
+ Requires mysqlplus.
6
+
7
+ require 'em/mysql'
8
+
9
+ # alias SQL for simpler syntax
10
+
11
+ SQL = EventedMysql
12
+ def SQL(query, &blk) SQL.select(query, &blk) end
13
+
14
+
15
+ # setup connection details and allow 4 connections to the server
16
+
17
+ SQL.settings.update :host => 'localhost',
18
+ :port => 3306,
19
+ :database => 'test',
20
+ :connections => 4
21
+
22
+
23
+ # use 4 connections to execute queries in parallel
24
+
25
+ SQL('select sleep(0.25)'){ p 'done' }
26
+ SQL('select sleep(0.25)'){ p 'done' }
27
+ SQL('select sleep(0.25)'){ p 'done' }
28
+ SQL('select sleep(0.25)'){ p 'done' }
29
+
30
+ Also includes a sequel async wrapper
31
+
32
+ require 'sequel'
33
+ require 'sequel/async'
34
+
35
+ DB = Sequel.connect(:adapter => 'mysql', :user => 'root', :database => 'test', ...)
36
+ EventedMysql.settings.update(..., :on_error => proc{|e| log 'error', e })
37
+
38
+ DB[:table].where(:field => 'value').async_update(:field => 'new value')
39
+
40
+ For more info, see the comments in lib/sequel/async.rb
data/em-mysql.gemspec ADDED
@@ -0,0 +1,21 @@
1
+ spec = Gem::Specification.new do |s|
2
+ s.name = 'superfeedr-em-mysql'
3
+ s.version = '0.4.0'
4
+ s.date = '2009-06-23'
5
+ s.summary = 'Async MySQL client API for Ruby/EventMachine'
6
+ s.email = "em-mysql@tmm1.net"
7
+ s.homepage = "http://github.com/tmm1/em-mysql"
8
+ s.description = 'Async MySQL client API for Ruby/EventMachine'
9
+ s.has_rdoc = false
10
+ s.authors = ["Aman Gupta"]
11
+ s.add_dependency('eventmachine', '>= 0.12.9')
12
+
13
+ # git ls-files
14
+ s.files = %w[
15
+ README
16
+ em-mysql.gemspec
17
+ lib/em/mysql.rb
18
+ lib/sequel/async.rb
19
+ test.rb
20
+ ]
21
+ end
data/lib/em/mysql.rb ADDED
@@ -0,0 +1,306 @@
1
+ require 'rubygems'
2
+ require 'eventmachine'
3
+ require 'mysqlplus'
4
+ require 'fcntl'
5
+
6
+ MAX_RETRIES_ON_DEADLOCKS = 10
7
+
8
+ class Mysql
9
+ def result
10
+ @cur_result
11
+ end
12
+ end
13
+
14
+ class EventedMysql < EM::Connection
15
+ def initialize mysql, opts
16
+ @mysql = mysql
17
+ @fd = mysql.socket
18
+ @opts = opts
19
+ @current = nil
20
+ @@queue ||= []
21
+ @processing = false
22
+ @connected = true
23
+
24
+ log 'mysql connected'
25
+
26
+ self.notify_readable = true
27
+ EM.add_timer(0){ next_query }
28
+ end
29
+ attr_reader :processing, :connected, :opts
30
+ alias :settings :opts
31
+
32
+ DisconnectErrors = [
33
+ 'query: not connected',
34
+ 'MySQL server has gone away',
35
+ 'Lost connection to MySQL server during query'
36
+ ] unless defined? DisconnectErrors
37
+
38
+ def notify_readable
39
+ log 'readable'
40
+ if item = @current
41
+ @current = nil
42
+ start, response, sql, cblk, eblk, retries = item
43
+ log 'mysql response', Time.now-start, sql
44
+ arg = case response
45
+ when :raw
46
+ result = @mysql.get_result
47
+ @mysql.instance_variable_set('@cur_result', result)
48
+ @mysql
49
+ when :select
50
+ ret = []
51
+ result = @mysql.get_result
52
+ result.each_hash{|h| ret << h }
53
+ log 'mysql result', ret
54
+ ret
55
+ when :update
56
+ result = @mysql.get_result
57
+ @mysql.affected_rows
58
+ when :insert
59
+ result = @mysql.get_result
60
+ @mysql.insert_id
61
+ else
62
+ result = @mysql.get_result
63
+ log 'got a result??', result if result
64
+ nil
65
+ end
66
+
67
+ @processing = false
68
+ # result.free if result.is_a? Mysql::Result
69
+ next_query
70
+ cblk.call(arg) if cblk
71
+ else
72
+ log 'readable, but nothing queued?! probably an ERROR state'
73
+ return close
74
+ end
75
+ rescue Mysql::Error => e
76
+ log 'mysql error', e.message
77
+ if e.message =~ /Deadlock/ and retries < MAX_RETRIES_ON_DEADLOCKS
78
+ @@queue << [response, sql, cblk, eblk, retries + 1]
79
+ @processing = false
80
+ next_query
81
+ elsif DisconnectErrors.include? e.message
82
+ @@queue << [response, sql, cblk, eblk, retries + 1]
83
+ return close
84
+ elsif cb = (eblk || @opts[:on_error])
85
+ cb.call(e)
86
+ @processing = false
87
+ next_query
88
+ else
89
+ raise e
90
+ end
91
+ # ensure
92
+ # res.free if res.is_a? Mysql::Result
93
+ # @processing = false
94
+ # next_query
95
+ end
96
+
97
+ def unbind
98
+ log 'mysql disconnect', $!, *($! ? $!.backtrace[0..5] : [])
99
+ # cp = EventedMysql.instance_variable_get('@connection_pool') and cp.delete(self)
100
+ @connected = false
101
+
102
+ # XXX wait for the next tick until the current fd is removed completely from the reactor
103
+ #
104
+ # XXX in certain cases the new FD# (@mysql.socket) is the same as the old, since FDs are re-used
105
+ # XXX without next_tick in these cases, unbind will get fired on the newly attached signature as well
106
+ #
107
+ # XXX do _NOT_ use EM.next_tick here. if a bunch of sockets disconnect at the same time, we want
108
+ # XXX reconnects to happen after all the unbinds have been processed
109
+ EM.add_timer(0) do
110
+ log 'mysql reconnecting'
111
+ @processing = false
112
+ @mysql = EventedMysql._connect @opts
113
+ @fd = @mysql.socket
114
+
115
+ @signature = EM.attach_fd @mysql.socket, true
116
+ EM.set_notify_readable @signature, true
117
+ log 'mysql connected'
118
+ EM.instance_variable_get('@conns')[@signature] = self
119
+ @connected = true
120
+ make_socket_blocking
121
+ next_query
122
+ end
123
+ end
124
+
125
+ def execute sql, response = nil, cblk = nil, eblk = nil, retries = 0, &blk
126
+ cblk ||= blk
127
+
128
+ begin
129
+ unless @processing or !@connected
130
+ # begin
131
+ # log 'mysql ping', @mysql.ping
132
+ # # log 'mysql stat', @mysql.stat
133
+ # # log 'mysql errno', @mysql.errno
134
+ # rescue
135
+ # log 'mysql ping failed'
136
+ # @@queue << [response, sql, blk]
137
+ # return close
138
+ # end
139
+
140
+ @processing = true
141
+
142
+ log 'mysql sending', sql
143
+ @mysql.send_query(sql)
144
+ else
145
+ @@queue << [response, sql, cblk, eblk, retries]
146
+ return
147
+ end
148
+ rescue Mysql::Error => e
149
+ log 'mysql error', e.message
150
+ if DisconnectErrors.include? e.message
151
+ @@queue << [response, sql, cblk, eblk, retries]
152
+ return close
153
+ else
154
+ raise e
155
+ end
156
+ end
157
+
158
+ log 'queuing', response, sql
159
+ @current = [Time.now, response, sql, cblk, eblk, retries]
160
+ end
161
+
162
+ def close
163
+ @connected = false
164
+ fd = detach
165
+ log 'detached fd', fd
166
+ end
167
+
168
+ private
169
+
170
+ def next_query
171
+ if @connected and !@processing and pending = @@queue.shift
172
+ response, sql, cblk, eblk = pending
173
+ execute(sql, response, cblk, eblk)
174
+ end
175
+ end
176
+
177
+ def log *args
178
+ return unless @opts[:logging]
179
+ p [Time.now, @fd, (@signature[-4..-1] if @signature), *args]
180
+ end
181
+
182
+ public
183
+
184
+ def self.connect opts
185
+ unless EM.respond_to?(:watch) and Mysql.method_defined?(:socket)
186
+ raise RuntimeError, 'mysqlplus and EM.watch are required for EventedMysql'
187
+ end
188
+
189
+ opts = servers_opts.pop
190
+ opts[:failover] = servers_opts
191
+ if conn = _connect(opts)
192
+ EM.watch conn.socket, self, conn, opts
193
+ else
194
+ EM.add_timer(5){ connect opts }
195
+ end
196
+ end
197
+
198
+ self::Mysql = ::Mysql unless defined? self::Mysql
199
+
200
+ # stolen from sequel
201
+ def self._connect opts
202
+ opts = settings.merge(opts)
203
+
204
+ conn = Mysql.init
205
+
206
+ # set encoding _before_ connecting
207
+ if charset = opts[:charset] || opts[:encoding]
208
+ conn.options(Mysql::SET_CHARSET_NAME, charset)
209
+ end
210
+
211
+ conn.options(Mysql::OPT_LOCAL_INFILE, 'client')
212
+
213
+ conn.real_connect(
214
+ opts[:host] || 'localhost',
215
+ opts[:user] || 'root',
216
+ opts[:password],
217
+ opts[:database],
218
+ opts[:port],
219
+ opts[:socket],
220
+ 0 +
221
+ # XXX multi results require multiple callbacks to parse
222
+ # Mysql::CLIENT_MULTI_RESULTS +
223
+ # Mysql::CLIENT_MULTI_STATEMENTS +
224
+ (opts[:compress] == false ? 0 : Mysql::CLIENT_COMPRESS)
225
+ )
226
+
227
+ # increase timeout so mysql server doesn't disconnect us
228
+ # this is especially bad if we're disconnected while EM.attach is
229
+ # still in progress, because by the time it gets to EM, the FD is
230
+ # no longer valid, and it throws a c++ 'bad file descriptor' error
231
+ # (do not use a timeout of -1 for unlimited, it does not work on mysqld > 5.0.60)
232
+ conn.query("set @@wait_timeout = #{opts[:timeout] || 2592000}")
233
+
234
+ # we handle reconnecting (and reattaching the new fd to EM)
235
+ conn.reconnect = false
236
+
237
+ # By default, MySQL 'where id is null' selects the last inserted id
238
+ # Turn this off. http://dev.rubyonrails.org/ticket/6778
239
+ conn.query("set SQL_AUTO_IS_NULL=0")
240
+
241
+ # get results for queries
242
+ conn.query_with_result = true
243
+
244
+ conn
245
+ rescue Mysql::Error => e
246
+ if cb = opts[:on_error]
247
+ cb.call(e)
248
+ nil
249
+ elsif opts[:failover]
250
+ connect(opts[:failover])
251
+ else
252
+ raise e
253
+ end
254
+ end
255
+ end
256
+
257
+ class EventedMysql
258
+ def self.settings
259
+ @settings ||= { :connections => 4, :logging => false }
260
+ end
261
+
262
+ def self.execute query, type = nil, cblk = nil, eblk = nil, &blk
263
+ unless nil#connection = connection_pool.find{|c| not c.processing and c.connected }
264
+ @n ||= 0
265
+ connection = connection_pool[@n]
266
+ @n = 0 if (@n+=1) >= connection_pool.size
267
+ end
268
+
269
+ connection.execute(query, type, cblk, eblk, &blk)
270
+ end
271
+
272
+ %w[ select insert update raw ].each do |type| class_eval %[
273
+
274
+ def self.#{type} query, cblk = nil, eblk = nil, &blk
275
+ execute query, :#{type}, cblk, eblk, &blk
276
+ end
277
+
278
+ ] end
279
+
280
+ def self.all query, type = nil, &blk
281
+ responses = 0
282
+ connection_pool.each do |c|
283
+ c.execute(query, type) do
284
+ responses += 1
285
+ blk.call if blk and responses == @connection_pool.size
286
+ end
287
+ end
288
+ end
289
+
290
+ def self.connection_pool
291
+ @connection_pool ||= (1..settings[:connections]).map{ EventedMysql.connect(settings) }
292
+ # p ['connpool', settings[:connections], @connection_pool.size]
293
+ # (1..(settings[:connections]-@connection_pool.size)).each do
294
+ # @connection_pool << EventedMysql.connect(settings)
295
+ # end unless settings[:connections] == @connection_pool.size
296
+ # @connection_pool
297
+ end
298
+
299
+ def self.reset!
300
+ @connection_pool.each do |c|
301
+ c.close
302
+ end
303
+ @connection_pool = nil
304
+ end
305
+ end
306
+
@@ -0,0 +1,184 @@
1
+ # async sequel extensions, for use with em-mysql
2
+ #
3
+ # require 'em/mysql'
4
+ # DB = Sequel.connect(:adapter => 'mysql', :user => 'root', :database => 'test', ...)
5
+ # EventedMysql.settings.update(..., :on_error => proc{|e| log 'error', e })
6
+ #
7
+ # def log *args
8
+ # p [Time.now, *args]
9
+ # end
10
+ #
11
+ # DB[:table].where(:id < 100).async_update(:field => 'new value') do |num_updated|
12
+ # log "done updating #{num_updated} rows"
13
+ # end
14
+ #
15
+ # DB[:table].async_insert(:field => 'value') do |insert_id|
16
+ # log "inserted row #{insert_id}"
17
+ # end
18
+ #
19
+ # DB[:table].async_multi_insert([:field], [ ['one'], ['two'], ['three'] ]) do
20
+ # log "done inserting 3 rows"
21
+ # end
22
+ #
23
+ # DB[:table].limit(10).async_each do |row|
24
+ # log "got a row", row
25
+ # end; log "this will be printed before the query returns"
26
+ #
27
+ # DB[:table].async_all do |rows|
28
+ # DB[:table].async_multi_insert([:field], rows.map{|r| "new_#{r[:field]}" })
29
+ # end
30
+ #
31
+ # DB[:table].async_all do |rows|
32
+ # num = rows.size
33
+ #
34
+ # rows.each{ |r|
35
+ # DB[:table].where(:id => r[:id]).async_update(:field => rand(10000).to_s) do
36
+ # num = num-1
37
+ # if num == 0
38
+ # log "last update completed"
39
+ # end
40
+ # end
41
+ # }
42
+ # end
43
+ #
44
+ # DB[:table].async_count do |num_rows|
45
+ # log "table has #{num_rows} rows"
46
+ # end
47
+
48
+ require 'sequel'
49
+ raise 'need Sequel >= 3.2.0' unless Sequel::MAJOR == 3 and Sequel::MINOR >= 2
50
+
51
+ module Sequel
52
+ class Dataset
53
+ def async_insert *args, &cb
54
+ EventedMysql.insert insert_sql(*args), &cb
55
+ nil
56
+ end
57
+
58
+ def async_insert_ignore *args, &cb
59
+ EventedMysql.insert insert_ignore.insert_sql(*args), &cb
60
+ nil
61
+ end
62
+
63
+ def async_update *args, &cb
64
+ EventedMysql.update update_sql(*args), &cb
65
+ nil
66
+ end
67
+
68
+ def async_delete &cb
69
+ EventedMysql.execute delete_sql, &cb
70
+ nil
71
+ end
72
+
73
+ def async_multi_insert *args, &cb
74
+ EventedMysql.execute multi_insert_sql(*args).first, &cb
75
+ nil
76
+ end
77
+
78
+ def async_multi_insert_ignore *args, &cb
79
+ EventedMysql.execute insert_ignore.multi_insert_sql(*args).first, &cb
80
+ nil
81
+ end
82
+
83
+ def async_fetch_rows sql, iter = :each
84
+ EventedMysql.raw(sql) do |m|
85
+ r = m.result
86
+
87
+ i = -1
88
+ cols = r.fetch_fields.map{|f| [output_identifier(f.name), Sequel::MySQL::MYSQL_TYPES[f.type], i+=1]}
89
+ @columns = cols.map{|c| c.first}
90
+ rows = []
91
+ while row = r.fetch_row
92
+ h = {}
93
+ cols.each{|n, p, i| v = row[i]; h[n] = (v && p) ? p.call(v) : v}
94
+ if iter == :each
95
+ yield h
96
+ else
97
+ rows << h
98
+ end
99
+ end
100
+ yield rows if iter == :all
101
+ end
102
+ nil
103
+ end
104
+
105
+ def async_each
106
+ async_fetch_rows(select_sql, :each) do |r|
107
+ if row_proc = @row_proc
108
+ yield row_proc.call(r)
109
+ else
110
+ yield r
111
+ end
112
+ end
113
+ nil
114
+ end
115
+
116
+ def async_all
117
+ async_fetch_rows(sql, :all) do |rows|
118
+ if row_proc = @row_proc
119
+ yield(rows.map{|r| row_proc.call(r) })
120
+ else
121
+ yield(rows)
122
+ end
123
+ end
124
+ nil
125
+ end
126
+
127
+ def async_count &cb
128
+ if options_overlap(COUNT_FROM_SELF_OPTS)
129
+ from_self.async_count(&cb)
130
+ else
131
+ clone(STOCK_COUNT_OPTS).async_each{|r|
132
+ yield r.is_a?(Hash) ? r.values.first.to_i : r.values.values.first.to_i
133
+ }
134
+ end
135
+ nil
136
+ end
137
+ end
138
+
139
+ class Model
140
+ def async_update *args, &cb
141
+ this.async_update(*args, &cb)
142
+ set(*args)
143
+ self
144
+ end
145
+
146
+ def async_delete &cb
147
+ this.async_delete(&cb)
148
+ nil
149
+ end
150
+
151
+ class << self
152
+ [ :async_insert,
153
+ :async_insert_ignore,
154
+ :async_multi_insert,
155
+ :async_multi_insert_ignore,
156
+ :async_each,
157
+ :async_all,
158
+ :async_update,
159
+ :async_count ].each do |method|
160
+ class_eval %[
161
+ def #{method} *args, &cb
162
+ dataset.#{method}(*args, &cb)
163
+ end
164
+ ]
165
+ end
166
+
167
+ # async version of Model#[]
168
+ def async_lookup args
169
+ unless Hash === args
170
+ args = primary_key_hash(args)
171
+ end
172
+
173
+ dataset.where(args).limit(1).async_all{ |rows|
174
+ if rows.any?
175
+ yield rows.first
176
+ else
177
+ yield nil
178
+ end
179
+ }
180
+ nil
181
+ end
182
+ end
183
+ end
184
+ end
data/test.rb ADDED
@@ -0,0 +1,59 @@
1
+ require 'lib/em/mysql'
2
+
3
+ # EM.kqueue
4
+ # EM.epoll
5
+ EM.run{
6
+ EM.start_server '127.0.0.1', 12345 do |c|
7
+ def c.receive_data data
8
+ p 'sending http response'
9
+ send_data "hello"
10
+ close_connection_after_writing
11
+ end
12
+ end
13
+
14
+ SQL = EventedMysql
15
+ def SQL(query, &blk) SQL.select(query, &blk) end
16
+
17
+ if false
18
+
19
+ SQL.settings.update :logging => true,
20
+ :database => 'test',
21
+ :connections => 1
22
+
23
+ SQL.execute('select 1+2')
24
+
25
+ EM.add_timer(1){
26
+ 3.times do SQL.select('select sleep(0.5)+1'){|r| p(r) } end
27
+ }
28
+
29
+ elsif false
30
+
31
+ SQL.settings.update :logging => true,
32
+ :database => 'test',
33
+ :connections => 10
34
+
35
+ EM.add_timer(2.5){ SQL.all('use test') }
36
+
37
+ else
38
+
39
+ SQL.settings.update :logging => true,
40
+ :database => 'test',
41
+ :connections => 10,
42
+ :timeout => 1
43
+
44
+ n = 0
45
+
46
+ SQL.execute('drop table if exists testingabc'){
47
+ SQL.execute('create table testingabc (a int, b int, c int)'){
48
+ EM.add_periodic_timer(0.2) do
49
+ cur_num = n+=1
50
+ SQL.execute("insert into testingabc values (1,2,#{cur_num})"){
51
+ SQL("select * from testingabc where c = #{cur_num} limit 1"){ |res| puts;puts }
52
+ }
53
+ end
54
+ }
55
+ }
56
+
57
+ end
58
+
59
+ }
metadata ADDED
@@ -0,0 +1,68 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: superfeedr-em-mysql
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.4.0
5
+ platform: ruby
6
+ authors:
7
+ - Aman Gupta
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-06-23 00:00:00 -07:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: eventmachine
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: 0.12.9
24
+ version:
25
+ description: Async MySQL client API for Ruby/EventMachine
26
+ email: em-mysql@tmm1.net
27
+ executables: []
28
+
29
+ extensions: []
30
+
31
+ extra_rdoc_files: []
32
+
33
+ files:
34
+ - README
35
+ - em-mysql.gemspec
36
+ - lib/em/mysql.rb
37
+ - lib/sequel/async.rb
38
+ - test.rb
39
+ has_rdoc: true
40
+ homepage: http://github.com/tmm1/em-mysql
41
+ licenses: []
42
+
43
+ post_install_message:
44
+ rdoc_options: []
45
+
46
+ require_paths:
47
+ - lib
48
+ required_ruby_version: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: "0"
53
+ version:
54
+ required_rubygems_version: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ version: "0"
59
+ version:
60
+ requirements: []
61
+
62
+ rubyforge_project:
63
+ rubygems_version: 1.3.5
64
+ signing_key:
65
+ specification_version: 3
66
+ summary: Async MySQL client API for Ruby/EventMachine
67
+ test_files: []
68
+