Skip to content

Commit 4584288

Browse files
committed
Add references from Client and Statement to each other to prevent out of order GC
1 parent 441b104 commit 4584288

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) { \
@@ -221,6 +221,7 @@ static void rb_mysql_client_mark(void * wrapper) {
221221
if (w) {
222222
rb_gc_mark_movable(w->encoding);
223223
rb_gc_mark_movable(w->active_fiber);
224+
rb_gc_mark_movable(w->prepared_statements);
224225
}
225226
}
226227

@@ -353,6 +354,14 @@ static VALUE invalidate_fd(int clientfd)
353354
}
354355
#endif /* _WIN32 */
355356

357+
static int decr_mysql2_stmt_hash(VALUE key, VALUE val, VALUE arg)
358+
{
359+
mysql_client_wrapper *wrapper = (mysql_client_wrapper *)arg;
360+
VALUE stmt = rb_ivar_get(wrapper->prepared_statements, key);
361+
// rb_funcall(stmt, rb_intern("close"), 0);
362+
return 0;
363+
}
364+
356365
static void *nogvl_close(void *ptr) {
357366
mysql_client_wrapper *wrapper = ptr;
358367

@@ -388,6 +397,8 @@ void decr_mysql2_client(mysql_client_wrapper *wrapper)
388397
}
389398
#endif
390399

400+
// rb_hash_foreach(wrapper->prepared_statements, decr_mysql2_stmt_hash, (VALUE)wrapper);
401+
391402
nogvl_close(wrapper);
392403
xfree(wrapper->client);
393404
xfree(wrapper);
@@ -404,6 +415,7 @@ static VALUE allocate(VALUE klass) {
404415
#endif
405416
wrapper->encoding = Qnil;
406417
wrapper->active_fiber = Qnil;
418+
wrapper->prepared_statements = rb_hash_new();
407419
wrapper->automatic_close = 1;
408420
wrapper->server_version = 0;
409421
wrapper->reconnect_enabled = 0;
@@ -1535,10 +1547,25 @@ static VALUE initialize_ext(VALUE self) {
15351547
* Create a new prepared statement.
15361548
*/
15371549
static VALUE rb_mysql_client_prepare_statement(VALUE self, VALUE sql) {
1550+
VALUE stmt;
15381551
GET_CLIENT(self);
15391552
REQUIRE_CONNECTED(wrapper);
15401553

1541-
return rb_mysql_stmt_new(self, sql);
1554+
stmt = rb_mysql_stmt_new(self, sql);
1555+
1556+
return stmt;
1557+
}
1558+
1559+
/* call-seq:
1560+
* client.prepared_statements
1561+
*
1562+
* Returns an array of prepared statement objects.
1563+
*/
1564+
static VALUE rb_mysql_client_prepared_statements_read(VALUE self) {
1565+
unsigned long retVal;
1566+
GET_CLIENT(self);
1567+
1568+
return rb_funcall(wrapper->prepared_statements, intern_values, 0);
15421569
}
15431570

15441571
void init_mysql2_client() {
@@ -1588,6 +1615,7 @@ void init_mysql2_client() {
15881615
rb_define_method(cMysql2Client, "last_id", rb_mysql_client_last_id, 0);
15891616
rb_define_method(cMysql2Client, "affected_rows", rb_mysql_client_affected_rows, 0);
15901617
rb_define_method(cMysql2Client, "prepare", rb_mysql_client_prepare_statement, 1);
1618+
rb_define_method(cMysql2Client, "prepared_statements", rb_mysql_client_prepared_statements_read, 0);
15911619
rb_define_method(cMysql2Client, "thread_id", rb_mysql_client_thread_id, 0);
15921620
rb_define_method(cMysql2Client, "ping", rb_mysql_client_ping, 0);
15931621
rb_define_method(cMysql2Client, "select_db", rb_mysql_client_select_db, 1);
@@ -1641,6 +1669,7 @@ void init_mysql2_client() {
16411669
intern_new_with_args = rb_intern("new_with_args");
16421670
intern_current_query_options = rb_intern("@current_query_options");
16431671
intern_read_timeout = rb_intern("@read_timeout");
1672+
intern_values = rb_intern("values");
16441673

16451674
#ifdef CLIENT_LONG_PASSWORD
16461675
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_fiber; /* rb_fiber_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
@@ -75,7 +75,15 @@ void decr_mysql2_stmt(mysql_stmt_wrapper *stmt_wrapper) {
7575
stmt_wrapper->refcount--;
7676

7777
if (stmt_wrapper->refcount == 0) {
78+
// If the GC get to client first it will be nil, and this cleanup won't matter
79+
if (stmt_wrapper->client_wrapper && stmt_wrapper->client_wrapper->refcount > 0) {
80+
// Remove the reference to this statement handle from the Client object.
81+
rb_hash_delete(stmt_wrapper->client_wrapper->prepared_statements,
82+
ULL2NUM((unsigned long long)stmt_wrapper));
83+
}
84+
7885
nogvl_stmt_close(stmt_wrapper);
86+
decr_mysql2_client(stmt_wrapper->client_wrapper);
7987
xfree(stmt_wrapper);
8088
}
8189
}
@@ -140,10 +148,18 @@ VALUE rb_mysql_stmt_new(VALUE rb_client, VALUE sql) {
140148
rb_stmt = Data_Make_Struct(cMysql2Statement, mysql_stmt_wrapper, rb_mysql_stmt_mark, rb_mysql_stmt_free, stmt_wrapper);
141149
#endif
142150
{
143-
stmt_wrapper->client = rb_client;
144151
stmt_wrapper->refcount = 1;
145152
stmt_wrapper->closed = 0;
146153
stmt_wrapper->stmt = NULL;
154+
155+
/* Keep a handle to the Client to ensure it doesn't get garbage collected first */
156+
stmt_wrapper->client = rb_client;
157+
if (rb_client != Qnil) {
158+
stmt_wrapper->client_wrapper = DATA_PTR(rb_client);
159+
stmt_wrapper->client_wrapper->refcount++;
160+
} else {
161+
stmt_wrapper->client_wrapper = NULL;
162+
}
147163
}
148164

149165
// instantiate stmt
@@ -178,6 +194,18 @@ VALUE rb_mysql_stmt_new(VALUE rb_client, VALUE sql) {
178194
}
179195
}
180196

197+
// Stash a reference to this statement handle into the Client to prevent
198+
// premature garbage collection.
199+
//
200+
// A statement can either be free explicitly or when the client object is
201+
// torn down. Freeing a statement handle at any other time causes protocol
202+
// traffic that might happen while the connection state is set for another
203+
// operation.
204+
{
205+
GET_CLIENT(rb_client);
206+
rb_hash_aset(wrapper->prepared_statements, ULL2NUM((unsigned long long)stmt_wrapper), rb_stmt);
207+
}
208+
181209
return rb_stmt;
182210
}
183211

@@ -609,7 +637,9 @@ static VALUE rb_mysql_stmt_close(VALUE self) {
609637
RAW_GET_STATEMENT(self);
610638

611639
if (!stmt_wrapper->closed) {
640+
GET_CLIENT(stmt_wrapper->client);
612641
stmt_wrapper->closed = 1;
642+
rb_hash_delete(wrapper->prepared_statements, ULL2NUM((unsigned long long)stmt_wrapper));
613643
rb_thread_call_without_gvl(nogvl_stmt_close, stmt_wrapper, RUBY_UBF_IO, 0);
614644
}
615645

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)