Skip to content

Commit 7c4de13

Browse files
committed
Add references from Client and Statement to each other to prevent out of order GC
1 parent 4e221cf commit 7c4de13

File tree

4 files changed

+66
-5
lines changed

4 files changed

+66
-5
lines changed

ext/mysql2/client.c

+31-2
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ extern VALUE mMysql2, cMysql2Error, cMysql2TimeoutError;
1919
static VALUE sym_id, sym_version, sym_header_version, sym_async, sym_symbolize_keys, sym_as, sym_array, sym_stream;
2020
static VALUE sym_no_good_index_used, sym_no_index_used, sym_query_was_slow;
2121
static ID intern_brackets, intern_merge, intern_merge_bang, intern_new_with_args,
22-
intern_current_query_options, intern_read_timeout;
22+
intern_current_query_options, intern_read_timeout, intern_values;
2323

2424
#define REQUIRE_INITIALIZED(wrapper) \
2525
if (!wrapper->initialized) { \
@@ -166,6 +166,7 @@ static void rb_mysql_client_mark(void * wrapper) {
166166
if (w) {
167167
rb_gc_mark(w->encoding);
168168
rb_gc_mark(w->active_thread);
169+
rb_gc_mark(w->prepared_statements);
169170
}
170171
}
171172

@@ -262,6 +263,14 @@ static VALUE invalidate_fd(int clientfd)
262263
}
263264
#endif /* _WIN32 */
264265

266+
static int decr_mysql2_stmt_hash(VALUE key, VALUE val, VALUE arg)
267+
{
268+
mysql_client_wrapper *wrapper = (mysql_client_wrapper *)arg;
269+
VALUE stmt = rb_ivar_get(wrapper->prepared_statements, key);
270+
// rb_funcall(stmt, rb_intern("close"), 0);
271+
return 0;
272+
}
273+
265274
static void *nogvl_close(void *ptr) {
266275
mysql_client_wrapper *wrapper = ptr;
267276

@@ -303,6 +312,8 @@ void decr_mysql2_client(mysql_client_wrapper *wrapper)
303312
}
304313
#endif
305314

315+
// rb_hash_foreach(wrapper->prepared_statements, decr_mysql2_stmt_hash, (VALUE)wrapper);
316+
306317
nogvl_close(wrapper);
307318
xfree(wrapper->client);
308319
xfree(wrapper);
@@ -315,6 +326,7 @@ static VALUE allocate(VALUE klass) {
315326
obj = Data_Make_Struct(klass, mysql_client_wrapper, rb_mysql_client_mark, rb_mysql_client_free, wrapper);
316327
wrapper->encoding = Qnil;
317328
wrapper->active_thread = Qnil;
329+
wrapper->prepared_statements = rb_hash_new();
318330
wrapper->automatic_close = 1;
319331
wrapper->server_version = 0;
320332
wrapper->reconnect_enabled = 0;
@@ -1371,10 +1383,25 @@ static VALUE initialize_ext(VALUE self) {
13711383
* Create a new prepared statement.
13721384
*/
13731385
static VALUE rb_mysql_client_prepare_statement(VALUE self, VALUE sql) {
1386+
VALUE stmt;
13741387
GET_CLIENT(self);
13751388
REQUIRE_CONNECTED(wrapper);
13761389

1377-
return rb_mysql_stmt_new(self, sql);
1390+
stmt = rb_mysql_stmt_new(self, sql);
1391+
1392+
return stmt;
1393+
}
1394+
1395+
/* call-seq:
1396+
* client.prepared_statements
1397+
*
1398+
* Returns an array of prepared statement objects.
1399+
*/
1400+
static VALUE rb_mysql_client_prepared_statements_read(VALUE self) {
1401+
unsigned long retVal;
1402+
GET_CLIENT(self);
1403+
1404+
return rb_funcall(wrapper->prepared_statements, intern_values, 0);
13781405
}
13791406

13801407
void init_mysql2_client() {
@@ -1423,6 +1450,7 @@ void init_mysql2_client() {
14231450
rb_define_method(cMysql2Client, "last_id", rb_mysql_client_last_id, 0);
14241451
rb_define_method(cMysql2Client, "affected_rows", rb_mysql_client_affected_rows, 0);
14251452
rb_define_method(cMysql2Client, "prepare", rb_mysql_client_prepare_statement, 1);
1453+
rb_define_method(cMysql2Client, "prepared_statements", rb_mysql_client_prepared_statements_read, 0);
14261454
rb_define_method(cMysql2Client, "thread_id", rb_mysql_client_thread_id, 0);
14271455
rb_define_method(cMysql2Client, "ping", rb_mysql_client_ping, 0);
14281456
rb_define_method(cMysql2Client, "select_db", rb_mysql_client_select_db, 1);
@@ -1474,6 +1502,7 @@ void init_mysql2_client() {
14741502
intern_new_with_args = rb_intern("new_with_args");
14751503
intern_current_query_options = rb_intern("@current_query_options");
14761504
intern_read_timeout = rb_intern("@read_timeout");
1505+
intern_values = rb_intern("values");
14771506

14781507
#ifdef CLIENT_LONG_PASSWORD
14791508
rb_const_set(cMysql2Client, rb_intern("LONG_PASSWORD"),

ext/mysql2/client.h

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
typedef struct {
55
VALUE encoding;
66
VALUE active_thread; /* rb_thread_current() or Qnil */
7+
VALUE prepared_statements;
78
long server_version;
89
int reconnect_enabled;
910
unsigned int connect_timeout;

ext/mysql2/statement.c

+31-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,15 @@ void decr_mysql2_stmt(mysql_stmt_wrapper *stmt_wrapper) {
3737
stmt_wrapper->refcount--;
3838

3939
if (stmt_wrapper->refcount == 0) {
40+
// If the GC get to client first it will be nil, and this cleanup won't matter
41+
if (stmt_wrapper->client_wrapper && stmt_wrapper->client_wrapper->refcount > 0) {
42+
// Remove the reference to this statement handle from the Client object.
43+
rb_hash_delete(stmt_wrapper->client_wrapper->prepared_statements,
44+
ULL2NUM((unsigned long long)stmt_wrapper));
45+
}
46+
4047
nogvl_stmt_close(stmt_wrapper);
48+
decr_mysql2_client(stmt_wrapper->client_wrapper);
4149
xfree(stmt_wrapper);
4250
}
4351
}
@@ -98,10 +106,18 @@ VALUE rb_mysql_stmt_new(VALUE rb_client, VALUE sql) {
98106

99107
rb_stmt = Data_Make_Struct(cMysql2Statement, mysql_stmt_wrapper, rb_mysql_stmt_mark, rb_mysql_stmt_free, stmt_wrapper);
100108
{
101-
stmt_wrapper->client = rb_client;
102109
stmt_wrapper->refcount = 1;
103110
stmt_wrapper->closed = 0;
104111
stmt_wrapper->stmt = NULL;
112+
113+
/* Keep a handle to the Client to ensure it doesn't get garbage collected first */
114+
stmt_wrapper->client = rb_client;
115+
if (rb_client != Qnil) {
116+
stmt_wrapper->client_wrapper = DATA_PTR(rb_client);
117+
stmt_wrapper->client_wrapper->refcount++;
118+
} else {
119+
stmt_wrapper->client_wrapper = NULL;
120+
}
105121
}
106122

107123
// instantiate stmt
@@ -136,6 +152,18 @@ VALUE rb_mysql_stmt_new(VALUE rb_client, VALUE sql) {
136152
}
137153
}
138154

155+
// Stash a reference to this statement handle into the Client to prevent
156+
// premature garbage collection.
157+
//
158+
// A statement can either be free explicitly or when the client object is
159+
// torn down. Freeing a statement handle at any other time causes protocol
160+
// traffic that might happen while the connection state is set for another
161+
// operation.
162+
{
163+
GET_CLIENT(rb_client);
164+
rb_hash_aset(wrapper->prepared_statements, ULL2NUM((unsigned long long)stmt_wrapper), rb_stmt);
165+
}
166+
139167
return rb_stmt;
140168
}
141169

@@ -565,7 +593,9 @@ static VALUE rb_mysql_stmt_affected_rows(VALUE self) {
565593
*/
566594
static VALUE rb_mysql_stmt_close(VALUE self) {
567595
GET_STATEMENT(self);
596+
GET_CLIENT(stmt_wrapper->client);
568597
stmt_wrapper->closed = 1;
598+
rb_hash_delete(wrapper->prepared_statements, ULL2NUM((unsigned long long)stmt_wrapper));
569599
rb_thread_call_without_gvl(nogvl_stmt_close, stmt_wrapper, RUBY_UBF_IO, 0);
570600
return Qnil;
571601
}

ext/mysql2/statement.h

+3-2
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@
22
#define MYSQL2_STATEMENT_H
33

44
typedef struct {
5+
int closed;
6+
int refcount;
57
VALUE client;
8+
mysql_client_wrapper *client_wrapper;
69
MYSQL_STMT *stmt;
7-
int refcount;
8-
int closed;
910
} mysql_stmt_wrapper;
1011

1112
void init_mysql2_statement(void);

0 commit comments

Comments
 (0)