mysql2 0.3.3 → 0.3.4
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.md +8 -0
- data/ext/mysql2/client.c +92 -51
- data/ext/mysql2/client.h +3 -2
- data/lib/mysql2/version.rb +2 -2
- data/spec/mysql2/client_spec.rb +102 -67
- data/spec/mysql2/result_spec.rb +6 -0
- metadata +4 -4
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,9 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
+
## 0.3.4 (June 15th, 2011)
|
4
|
+
* fix a long standing bug where a signal would interrupt rb_thread_select and put the connection in a permanently broken state
|
5
|
+
* turn on casting in the ActiveRecord again, users can disable it if they need to for performance reasons
|
6
|
+
|
3
7
|
## 0.3.3 (June 14th, 2011)
|
4
8
|
* disable async support, and access to the underlying file descriptor under Windows. It's never worked reliably and ruby-core has a lot of work to do in order to make it possible.
|
5
9
|
* added support for turning eager-casting off. This is especially useful in ORMs that will lazily cast values upon access.
|
@@ -19,6 +23,10 @@
|
|
19
23
|
* BREAKING CHANGE: the ActiveRecord adapter has been pulled into Rails 3.1 and is no longer part of the gem
|
20
24
|
* added Mysql2::Client.escape (class-level) for raw one-off non-encoding-aware escaping
|
21
25
|
|
26
|
+
## 0.2.9 (June 15th, 2011)
|
27
|
+
* fix a long standing bug where a signal would interrupt rb_thread_select and put the connection in a permanently broken state
|
28
|
+
* turn on casting in the ActiveRecord again, users can disable it if they need to for performance reasons
|
29
|
+
|
22
30
|
## 0.2.8 (June 14th, 2011)
|
23
31
|
* disable async support, and access to the underlying file descriptor under Windows. It's never worked reliably and ruby-core has a lot of work to do in order to make it possible.
|
24
32
|
* added support for turning eager-casting off. This is especially useful in ORMs that will lazily cast values upon access.
|
data/ext/mysql2/client.c
CHANGED
@@ -1,6 +1,9 @@
|
|
1
1
|
#include <mysql2_ext.h>
|
2
2
|
#include <client.h>
|
3
3
|
#include <errno.h>
|
4
|
+
#ifndef _WIN32
|
5
|
+
#include <sys/socket.h>
|
6
|
+
#endif
|
4
7
|
|
5
8
|
VALUE cMysql2Client;
|
6
9
|
extern VALUE mMysql2, cMysql2Error;
|
@@ -9,7 +12,7 @@ static VALUE sym_id, sym_version, sym_async, sym_symbolize_keys, sym_as, sym_arr
|
|
9
12
|
static ID intern_merge, intern_error_number_eql, intern_sql_state_eql;
|
10
13
|
|
11
14
|
#define REQUIRE_OPEN_DB(wrapper) \
|
12
|
-
if(wrapper->closed) { \
|
15
|
+
if(!wrapper->reconnect_enabled && wrapper->closed) { \
|
13
16
|
rb_raise(cMysql2Error, "closed MySQL connection"); \
|
14
17
|
}
|
15
18
|
|
@@ -125,7 +128,7 @@ static VALUE nogvl_close(void *ptr) {
|
|
125
128
|
wrapper = ptr;
|
126
129
|
if (!wrapper->closed) {
|
127
130
|
wrapper->closed = 1;
|
128
|
-
|
131
|
+
wrapper->active = 0;
|
129
132
|
/*
|
130
133
|
* we'll send a QUIT message to the server, but that message is more of a
|
131
134
|
* formality than a hard requirement since the socket is getting shutdown
|
@@ -162,6 +165,7 @@ static VALUE allocate(VALUE klass) {
|
|
162
165
|
obj = Data_Make_Struct(klass, mysql_client_wrapper, rb_mysql_client_mark, rb_mysql_client_free, wrapper);
|
163
166
|
wrapper->encoding = Qnil;
|
164
167
|
wrapper->active = 0;
|
168
|
+
wrapper->reconnect_enabled = 0;
|
165
169
|
wrapper->closed = 1;
|
166
170
|
wrapper->client = (MYSQL*)xmalloc(sizeof(MYSQL));
|
167
171
|
return obj;
|
@@ -271,6 +275,10 @@ static VALUE rb_mysql_client_async_result(VALUE self) {
|
|
271
275
|
#endif
|
272
276
|
GET_CLIENT(self);
|
273
277
|
|
278
|
+
// if we're not waiting on a result, do nothing
|
279
|
+
if (!wrapper->active)
|
280
|
+
return Qnil;
|
281
|
+
|
274
282
|
REQUIRE_OPEN_DB(wrapper);
|
275
283
|
if (rb_thread_blocking_region(nogvl_read_query_result, wrapper->client, RUBY_UBF_IO, 0) == Qfalse) {
|
276
284
|
// an error occurred, mark this connection inactive
|
@@ -301,19 +309,89 @@ static VALUE rb_mysql_client_async_result(VALUE self) {
|
|
301
309
|
return resultObj;
|
302
310
|
}
|
303
311
|
|
312
|
+
#ifndef _WIN32
|
313
|
+
struct async_query_args {
|
314
|
+
int fd;
|
315
|
+
VALUE self;
|
316
|
+
};
|
317
|
+
|
318
|
+
static VALUE disconnect_and_raise(VALUE self, VALUE error) {
|
319
|
+
GET_CLIENT(self);
|
320
|
+
|
321
|
+
wrapper->closed = 1;
|
322
|
+
wrapper->active = 0;
|
323
|
+
|
324
|
+
// manually close the socket for read/write
|
325
|
+
// this feels dirty, but is there another way?
|
326
|
+
shutdown(wrapper->client->net.fd, 2);
|
327
|
+
|
328
|
+
rb_exc_raise(error);
|
329
|
+
|
330
|
+
return Qnil;
|
331
|
+
}
|
332
|
+
#endif
|
333
|
+
|
334
|
+
static VALUE do_query(void *args) {
|
335
|
+
struct async_query_args *async_args;
|
336
|
+
struct timeval tv;
|
337
|
+
struct timeval* tvp;
|
338
|
+
long int sec;
|
339
|
+
fd_set fdset;
|
340
|
+
int retval;
|
341
|
+
int fd_set_fd;
|
342
|
+
VALUE read_timeout;
|
343
|
+
|
344
|
+
async_args = (struct async_query_args *)args;
|
345
|
+
read_timeout = rb_iv_get(async_args->self, "@read_timeout");
|
346
|
+
|
347
|
+
tvp = NULL;
|
348
|
+
if (!NIL_P(read_timeout)) {
|
349
|
+
Check_Type(read_timeout, T_FIXNUM);
|
350
|
+
tvp = &tv;
|
351
|
+
sec = FIX2INT(read_timeout);
|
352
|
+
// TODO: support partial seconds?
|
353
|
+
// also, this check is here for sanity, we also check up in Ruby
|
354
|
+
if (sec >= 0) {
|
355
|
+
tvp->tv_sec = sec;
|
356
|
+
} else {
|
357
|
+
rb_raise(cMysql2Error, "read_timeout must be a positive integer, you passed %ld", sec);
|
358
|
+
}
|
359
|
+
tvp->tv_usec = 0;
|
360
|
+
}
|
361
|
+
|
362
|
+
fd_set_fd = async_args->fd;
|
363
|
+
for(;;) {
|
364
|
+
// the below code is largely from do_mysql
|
365
|
+
// http://github.com/datamapper/do
|
366
|
+
FD_ZERO(&fdset);
|
367
|
+
FD_SET(fd_set_fd, &fdset);
|
368
|
+
|
369
|
+
retval = rb_thread_select(fd_set_fd + 1, &fdset, NULL, NULL, tvp);
|
370
|
+
|
371
|
+
if (retval == 0) {
|
372
|
+
rb_raise(cMysql2Error, "Timeout waiting for a response from the last query. (waited %d seconds)", FIX2INT(read_timeout));
|
373
|
+
}
|
374
|
+
|
375
|
+
if (retval < 0) {
|
376
|
+
rb_sys_fail(0);
|
377
|
+
}
|
378
|
+
|
379
|
+
if (retval > 0) {
|
380
|
+
break;
|
381
|
+
}
|
382
|
+
}
|
383
|
+
|
384
|
+
return Qnil;
|
385
|
+
}
|
386
|
+
|
304
387
|
static VALUE rb_mysql_client_query(int argc, VALUE * argv, VALUE self) {
|
388
|
+
struct async_query_args async_args;
|
305
389
|
struct nogvl_send_query_args args;
|
306
|
-
fd_set fdset;
|
307
|
-
int fd, retval;
|
308
390
|
int async = 0;
|
309
|
-
VALUE opts, defaults
|
391
|
+
VALUE opts, defaults;
|
310
392
|
#ifdef HAVE_RUBY_ENCODING_H
|
311
393
|
rb_encoding *conn_enc;
|
312
394
|
#endif
|
313
|
-
struct timeval tv;
|
314
|
-
struct timeval* tvp;
|
315
|
-
long int sec;
|
316
|
-
VALUE result;
|
317
395
|
GET_CLIENT(self);
|
318
396
|
|
319
397
|
REQUIRE_OPEN_DB(wrapper);
|
@@ -353,51 +431,13 @@ static VALUE rb_mysql_client_query(int argc, VALUE * argv, VALUE self) {
|
|
353
431
|
}
|
354
432
|
|
355
433
|
#ifndef _WIN32
|
356
|
-
read_timeout = rb_iv_get(self, "@read_timeout");
|
357
|
-
|
358
|
-
tvp = NULL;
|
359
|
-
if (!NIL_P(read_timeout)) {
|
360
|
-
Check_Type(read_timeout, T_FIXNUM);
|
361
|
-
tvp = &tv;
|
362
|
-
sec = FIX2INT(read_timeout);
|
363
|
-
// TODO: support partial seconds?
|
364
|
-
// also, this check is here for sanity, we also check up in Ruby
|
365
|
-
if (sec >= 0) {
|
366
|
-
tvp->tv_sec = sec;
|
367
|
-
} else {
|
368
|
-
rb_raise(cMysql2Error, "read_timeout must be a positive integer, you passed %ld", sec);
|
369
|
-
}
|
370
|
-
tvp->tv_usec = 0;
|
371
|
-
}
|
372
|
-
|
373
434
|
if (!async) {
|
374
|
-
|
375
|
-
|
376
|
-
fd = wrapper->client->net.fd;
|
377
|
-
for(;;) {
|
378
|
-
int fd_set_fd = fd;
|
379
|
-
|
380
|
-
FD_ZERO(&fdset);
|
381
|
-
FD_SET(fd_set_fd, &fdset);
|
382
|
-
|
383
|
-
retval = rb_thread_select(fd_set_fd + 1, &fdset, NULL, NULL, tvp);
|
384
|
-
|
385
|
-
if (retval == 0) {
|
386
|
-
rb_raise(cMysql2Error, "Timeout waiting for a response from the last query. (waited %d seconds)", FIX2INT(read_timeout));
|
387
|
-
}
|
388
|
-
|
389
|
-
if (retval < 0) {
|
390
|
-
rb_sys_fail(0);
|
391
|
-
}
|
392
|
-
|
393
|
-
if (retval > 0) {
|
394
|
-
break;
|
395
|
-
}
|
396
|
-
}
|
435
|
+
async_args.fd = wrapper->client->net.fd;
|
436
|
+
async_args.self = self;
|
397
437
|
|
398
|
-
|
438
|
+
rb_rescue2(do_query, (VALUE)&async_args, disconnect_and_raise, self, rb_eException, (VALUE)0);
|
399
439
|
|
400
|
-
return
|
440
|
+
return rb_mysql_client_async_result(self);
|
401
441
|
} else {
|
402
442
|
return Qnil;
|
403
443
|
}
|
@@ -568,6 +608,7 @@ static VALUE set_reconnect(VALUE self, VALUE value) {
|
|
568
608
|
if(!NIL_P(value)) {
|
569
609
|
reconnect = value == Qfalse ? 0 : 1;
|
570
610
|
|
611
|
+
wrapper->reconnect_enabled = reconnect;
|
571
612
|
/* set default reconnect behavior */
|
572
613
|
if (mysql_options(wrapper->client, MYSQL_OPT_RECONNECT, &reconnect)) {
|
573
614
|
/* TODO: warning - unable to set reconnect behavior */
|
data/ext/mysql2/client.h
CHANGED
data/lib/mysql2/version.rb
CHANGED
@@ -1,3 +1,3 @@
|
|
1
1
|
module Mysql2
|
2
|
-
VERSION = "0.3.
|
3
|
-
end
|
2
|
+
VERSION = "0.3.4"
|
3
|
+
end
|
data/spec/mysql2/client_spec.rb
CHANGED
@@ -116,13 +116,6 @@ describe Mysql2::Client do
|
|
116
116
|
@client.query("SELECT 1", :symbolize_keys => true).first.keys[0].class.should eql(Symbol)
|
117
117
|
end
|
118
118
|
|
119
|
-
it "should not allow another query to be sent without fetching a result first" do
|
120
|
-
@client.query("SELECT 1", :async => true)
|
121
|
-
lambda {
|
122
|
-
@client.query("SELECT 1")
|
123
|
-
}.should raise_error(Mysql2::Error)
|
124
|
-
end
|
125
|
-
|
126
119
|
it "should require an open connection" do
|
127
120
|
@client.close
|
128
121
|
lambda {
|
@@ -130,16 +123,23 @@ describe Mysql2::Client do
|
|
130
123
|
}.should raise_error(Mysql2::Error)
|
131
124
|
end
|
132
125
|
|
133
|
-
it "should timeout if we wait longer than :read_timeout" do
|
134
|
-
client = Mysql2::Client.new(:read_timeout => 1)
|
135
|
-
lambda {
|
136
|
-
client.query("SELECT sleep(2)")
|
137
|
-
}.should raise_error(Mysql2::Error)
|
138
|
-
end
|
139
|
-
|
140
|
-
# XXX this test is not deterministic (because Unix signal handling is not)
|
141
|
-
# and may fail on a loaded system
|
142
126
|
if RUBY_PLATFORM !~ /mingw|mswin/
|
127
|
+
it "should not allow another query to be sent without fetching a result first" do
|
128
|
+
@client.query("SELECT 1", :async => true)
|
129
|
+
lambda {
|
130
|
+
@client.query("SELECT 1")
|
131
|
+
}.should raise_error(Mysql2::Error)
|
132
|
+
end
|
133
|
+
|
134
|
+
it "should timeout if we wait longer than :read_timeout" do
|
135
|
+
client = Mysql2::Client.new(:read_timeout => 1)
|
136
|
+
lambda {
|
137
|
+
client.query("SELECT sleep(2)")
|
138
|
+
}.should raise_error(Mysql2::Error)
|
139
|
+
end
|
140
|
+
|
141
|
+
# XXX this test is not deterministic (because Unix signal handling is not)
|
142
|
+
# and may fail on a loaded system
|
143
143
|
it "should run signal handlers while waiting for a response" do
|
144
144
|
mark = {}
|
145
145
|
trap(:USR1) { mark[:USR1] = Time.now }
|
@@ -164,6 +164,92 @@ describe Mysql2::Client do
|
|
164
164
|
trap(:USR1, 'DEFAULT')
|
165
165
|
end
|
166
166
|
end
|
167
|
+
|
168
|
+
it "#socket should return a Fixnum (file descriptor from C)" do
|
169
|
+
@client.socket.class.should eql(Fixnum)
|
170
|
+
@client.socket.should_not eql(0)
|
171
|
+
end
|
172
|
+
|
173
|
+
it "#socket should require an open connection" do
|
174
|
+
@client.close
|
175
|
+
lambda {
|
176
|
+
@client.socket
|
177
|
+
}.should raise_error(Mysql2::Error)
|
178
|
+
end
|
179
|
+
|
180
|
+
it "should close the connection when an exception is raised" do
|
181
|
+
begin
|
182
|
+
Timeout.timeout(1) do
|
183
|
+
@client.query("SELECT sleep(2)")
|
184
|
+
end
|
185
|
+
rescue Timeout::Error
|
186
|
+
end
|
187
|
+
|
188
|
+
lambda {
|
189
|
+
@client.query("SELECT 1")
|
190
|
+
}.should raise_error(Mysql2::Error, 'closed MySQL connection')
|
191
|
+
end
|
192
|
+
|
193
|
+
it "should handle Timeouts without leaving the connection hanging if reconnect is true" do
|
194
|
+
client = Mysql2::Client.new(:reconnect => true)
|
195
|
+
begin
|
196
|
+
Timeout.timeout(1) do
|
197
|
+
client.query("SELECT sleep(2)")
|
198
|
+
end
|
199
|
+
rescue Timeout::Error
|
200
|
+
end
|
201
|
+
|
202
|
+
lambda {
|
203
|
+
client.query("SELECT 1")
|
204
|
+
}.should_not raise_error(Mysql2::Error)
|
205
|
+
end
|
206
|
+
|
207
|
+
it "threaded queries should be supported" do
|
208
|
+
threads, results = [], {}
|
209
|
+
connect = lambda{ Mysql2::Client.new(:host => "localhost", :username => "root") }
|
210
|
+
Timeout.timeout(0.7) do
|
211
|
+
5.times {
|
212
|
+
threads << Thread.new do
|
213
|
+
results[Thread.current.object_id] = connect.call.query("SELECT sleep(0.5) as result")
|
214
|
+
end
|
215
|
+
}
|
216
|
+
end
|
217
|
+
threads.each{|t| t.join }
|
218
|
+
results.keys.sort.should eql(threads.map{|t| t.object_id }.sort)
|
219
|
+
end
|
220
|
+
|
221
|
+
it "evented async queries should be supported" do
|
222
|
+
# should immediately return nil
|
223
|
+
@client.query("SELECT sleep(0.1)", :async => true).should eql(nil)
|
224
|
+
|
225
|
+
io_wrapper = IO.for_fd(@client.socket)
|
226
|
+
loops = 0
|
227
|
+
loop do
|
228
|
+
if IO.select([io_wrapper], nil, nil, 0.05)
|
229
|
+
break
|
230
|
+
else
|
231
|
+
loops += 1
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
# make sure we waited some period of time
|
236
|
+
(loops >= 1).should be_true
|
237
|
+
|
238
|
+
result = @client.async_result
|
239
|
+
result.class.should eql(Mysql2::Result)
|
240
|
+
end
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
it "should respond to #socket" do
|
245
|
+
@client.should respond_to(:socket)
|
246
|
+
end
|
247
|
+
|
248
|
+
if RUBY_PLATFORM =~ /mingw|mswin/
|
249
|
+
it "#socket should raise as it's not supported" do
|
250
|
+
lambda {
|
251
|
+
@client.socket
|
252
|
+
}.should raise_error(Mysql2::Error)
|
167
253
|
end
|
168
254
|
end
|
169
255
|
|
@@ -311,22 +397,6 @@ describe Mysql2::Client do
|
|
311
397
|
end
|
312
398
|
end
|
313
399
|
|
314
|
-
it "should respond to #socket" do
|
315
|
-
@client.should respond_to(:socket)
|
316
|
-
end
|
317
|
-
|
318
|
-
it "#socket should return a Fixnum (file descriptor from C)" do
|
319
|
-
@client.socket.class.should eql(Fixnum)
|
320
|
-
@client.socket.should_not eql(0)
|
321
|
-
end
|
322
|
-
|
323
|
-
it "#socket should require an open connection" do
|
324
|
-
@client.close
|
325
|
-
lambda {
|
326
|
-
@client.socket
|
327
|
-
}.should raise_error(Mysql2::Error)
|
328
|
-
end
|
329
|
-
|
330
400
|
it "should raise a Mysql2::Error exception upon connection failure" do
|
331
401
|
lambda {
|
332
402
|
bad_client = Mysql2::Client.new :host => "localhost", :username => 'asdfasdf8d2h', :password => 'asdfasdfw42'
|
@@ -337,41 +407,6 @@ describe Mysql2::Client do
|
|
337
407
|
}.should_not raise_error(Mysql2::Error)
|
338
408
|
end
|
339
409
|
|
340
|
-
it "threaded queries should be supported" do
|
341
|
-
threads, results = [], {}
|
342
|
-
connect = lambda{ Mysql2::Client.new(:host => "localhost", :username => "root") }
|
343
|
-
Timeout.timeout(0.7) do
|
344
|
-
5.times {
|
345
|
-
threads << Thread.new do
|
346
|
-
results[Thread.current.object_id] = connect.call.query("SELECT sleep(0.5) as result")
|
347
|
-
end
|
348
|
-
}
|
349
|
-
end
|
350
|
-
threads.each{|t| t.join }
|
351
|
-
results.keys.sort.should eql(threads.map{|t| t.object_id }.sort)
|
352
|
-
end
|
353
|
-
|
354
|
-
it "evented async queries should be supported" do
|
355
|
-
# should immediately return nil
|
356
|
-
@client.query("SELECT sleep(0.1)", :async => true).should eql(nil)
|
357
|
-
|
358
|
-
io_wrapper = IO.for_fd(@client.socket)
|
359
|
-
loops = 0
|
360
|
-
loop do
|
361
|
-
if IO.select([io_wrapper], nil, nil, 0.05)
|
362
|
-
break
|
363
|
-
else
|
364
|
-
loops += 1
|
365
|
-
end
|
366
|
-
end
|
367
|
-
|
368
|
-
# make sure we waited some period of time
|
369
|
-
(loops >= 1).should be_true
|
370
|
-
|
371
|
-
result = @client.async_result
|
372
|
-
result.class.should eql(Mysql2::Result)
|
373
|
-
end
|
374
|
-
|
375
410
|
context 'write operations api' do
|
376
411
|
before(:each) do
|
377
412
|
@client.query "USE test"
|
data/spec/mysql2/result_spec.rb
CHANGED
@@ -28,6 +28,12 @@ describe Mysql2::Result do
|
|
28
28
|
}.should_not raise_error(Mysql2::Error)
|
29
29
|
end
|
30
30
|
|
31
|
+
context "metadata queries" do
|
32
|
+
it "should show tables" do
|
33
|
+
@result = @client.query "SHOW TABLES"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
31
37
|
context "#each" do
|
32
38
|
it "should yield rows as hash's" do
|
33
39
|
@result.each do |row|
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: mysql2
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 27
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 0
|
8
8
|
- 3
|
9
|
-
-
|
10
|
-
version: 0.3.
|
9
|
+
- 4
|
10
|
+
version: 0.3.4
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Brian Lopez
|
@@ -15,7 +15,7 @@ autorequire:
|
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
17
|
|
18
|
-
date: 2011-06-
|
18
|
+
date: 2011-06-16 00:00:00 -07:00
|
19
19
|
default_executable:
|
20
20
|
dependencies:
|
21
21
|
- !ruby/object:Gem::Dependency
|