add Mysql2::Client#close method

This allows users to (manually) avoid the tricky case of hitting
a socket write during GC and potentially blocking the
interpreter during GC.  An explicit `close' is also useful in
situations where server resources are limited and a client only
needs to connect for a limited time.
This commit is contained in:
Eric Wong 2010-05-05 17:38:08 -07:00
parent f7a6b49cbd
commit 1d92db8aab
3 changed files with 68 additions and 14 deletions

View File

@ -22,6 +22,15 @@
* - mysql_ssl_set() * - mysql_ssl_set()
*/ */
static VALUE nogvl_init(void *ptr) {
struct nogvl_connect_args *args = ptr;
/* may initialize embedded server and read /etc/services off disk */
args->mysql = mysql_init(NULL);
return args->mysql == NULL ? Qfalse : Qtrue;
}
static VALUE nogvl_connect(void *ptr) static VALUE nogvl_connect(void *ptr)
{ {
struct nogvl_connect_args *args = ptr; struct nogvl_connect_args *args = ptr;
@ -37,6 +46,7 @@ static VALUE nogvl_connect(void *ptr)
/* Mysql2::Client */ /* Mysql2::Client */
static VALUE rb_mysql_client_new(int argc, VALUE * argv, VALUE klass) { static VALUE rb_mysql_client_new(int argc, VALUE * argv, VALUE klass) {
mysql2_client_wrapper * client;
struct nogvl_connect_args args = { struct nogvl_connect_args args = {
.host = "localhost", .host = "localhost",
.user = NULL, .user = NULL,
@ -57,7 +67,7 @@ static VALUE rb_mysql_client_new(int argc, VALUE * argv, VALUE klass) {
unsigned int connect_timeout = 0; unsigned int connect_timeout = 0;
my_bool reconnect = 1; my_bool reconnect = 1;
obj = Data_Make_Struct(klass, MYSQL, NULL, rb_mysql_client_free, args.mysql); obj = Data_Make_Struct(klass, mysql2_client_wrapper, NULL, rb_mysql_client_free, client);
if (rb_scan_args(argc, argv, "01", &opts) == 1) { if (rb_scan_args(argc, argv, "01", &opts) == 1) {
Check_Type(opts, T_HASH); Check_Type(opts, T_HASH);
@ -128,12 +138,7 @@ static VALUE rb_mysql_client_new(int argc, VALUE * argv, VALUE klass) {
} }
} }
/* if (rb_thread_blocking_region(nogvl_init, &args, RUBY_UBF_IO, 0) == Qfalse) {
* FIXME: mysql_init may initialize the embedded server and read
* /etc/services off disk (always blocking), so in those unlikely
* cases we should make this release the GVL
*/
if (!mysql_init(args.mysql)) {
// TODO: warning - not enough memory? // TODO: warning - not enough memory?
rb_raise(cMysql2Error, "%s", mysql_error(args.mysql)); rb_raise(cMysql2Error, "%s", mysql_error(args.mysql));
return Qnil; return Qnil;
@ -167,6 +172,8 @@ static VALUE rb_mysql_client_new(int argc, VALUE * argv, VALUE klass) {
return Qnil; return Qnil;
} }
client->client = args.mysql;
rb_obj_call_init(obj, argc, argv); rb_obj_call_init(obj, argc, argv);
return obj; return obj;
} }
@ -175,15 +182,43 @@ static VALUE rb_mysql_client_init(RB_MYSQL_UNUSED int argc, RB_MYSQL_UNUSED VALU
return self; return self;
} }
static void rb_mysql_client_free(void * client) { static void rb_mysql_client_free(void * ptr) {
MYSQL * c = client; mysql2_client_wrapper * client = ptr;
if (c) {
if (client->client) {
/* /*
* FIXME: this may send a "QUIT" message to the server and thus block * this may send a "QUIT" message to the server and thus block
* on the socket write) * on the socket write, users are encouraged to close this manually
* to avoid this behavior
*/ */
mysql_close(client); mysql_close(client->client);
} }
xfree(ptr);
}
static VALUE nogvl_close(void * ptr) {
mysql_close((MYSQL *)ptr);
return Qnil;
}
/*
* Immediately disconnect from the server, normally the garbage collector
* will disconnect automatically when a connection is no longer needed.
* Explicitly closing this can free up server resources sooner and is
* also 100% safe when faced with signals or running multithreaded
*/
static VALUE rb_mysql_client_close(VALUE self) {
mysql2_client_wrapper *client;
Data_Get_Struct(self, mysql2_client_wrapper, client);
if (client->client) {
rb_thread_blocking_region(nogvl_close, client->client, RUBY_UBF_IO, 0);
client->client = NULL;
} else {
rb_raise(cMysql2Error, "already closed MySQL connection");
}
return Qnil;
} }
/* /*
@ -607,6 +642,7 @@ void Init_mysql2_ext() {
VALUE cMysql2Client = rb_define_class_under(mMysql2, "Client", rb_cObject); VALUE cMysql2Client = rb_define_class_under(mMysql2, "Client", rb_cObject);
rb_define_singleton_method(cMysql2Client, "new", rb_mysql_client_new, -1); rb_define_singleton_method(cMysql2Client, "new", rb_mysql_client_new, -1);
rb_define_method(cMysql2Client, "initialize", rb_mysql_client_init, -1); rb_define_method(cMysql2Client, "initialize", rb_mysql_client_init, -1);
rb_define_method(cMysql2Client, "close", rb_mysql_client_close, 0);
rb_define_method(cMysql2Client, "query", rb_mysql_client_query, -1); rb_define_method(cMysql2Client, "query", rb_mysql_client_query, -1);
rb_define_method(cMysql2Client, "escape", rb_mysql_client_escape, 1); rb_define_method(cMysql2Client, "escape", rb_mysql_client_escape, 1);
rb_define_method(cMysql2Client, "info", rb_mysql_client_info, 0); rb_define_method(cMysql2Client, "info", rb_mysql_client_info, 0);

View File

@ -30,7 +30,10 @@ static ID intern_new, intern_local;
static VALUE cMysql2Error; static VALUE cMysql2Error;
/* Mysql2::Client */ /* Mysql2::Client */
#define GetMysql2Client(obj, sval) (sval = (MYSQL*)DATA_PTR(obj)); typedef struct {
MYSQL * client;
} mysql2_client_wrapper;
#define GetMysql2Client(obj, sval) (sval = ((mysql2_client_wrapper*)(DATA_PTR(obj)))->client);
static ID sym_socket, sym_host, sym_port, sym_username, sym_password, static ID sym_socket, sym_host, sym_port, sym_username, sym_password,
sym_database, sym_reconnect, sym_connect_timeout, sym_id, sym_version, sym_database, sym_reconnect, sym_connect_timeout, sym_id, sym_version,
sym_sslkey, sym_sslcert, sym_sslca, sym_sslcapath, sym_sslcipher, sym_sslkey, sym_sslcert, sym_sslca, sym_sslcapath, sym_sslcipher,

View File

@ -34,6 +34,21 @@ describe Mysql2::Client do
results[1]['Value'].class.should eql(String) results[1]['Value'].class.should eql(String)
end end
it "should respond to #close" do
@client.should respond_to :close
end
it "should be able to close properly" do
@client.close.should be_nil
end
it "should raise an exception when closed twice" do
@client.close.should be_nil
lambda {
@client.close
}.should raise_error(Mysql2::Error)
end
it "should respond to #query" do it "should respond to #query" do
@client.should respond_to :query @client.should respond_to :query
end end