Skip to content

Commit de4e1bd

Browse files
committed
Merge pull request #636 from sodabrew/stmt_gc_fix
Statement GC fixes
2 parents d0fef7c + c0740eb commit de4e1bd

File tree

7 files changed

+83
-30
lines changed

7 files changed

+83
-30
lines changed

ext/mysql2/client.c

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -490,7 +490,7 @@ static VALUE rb_mysql_client_async_result(VALUE self) {
490490
current = rb_hash_dup(rb_iv_get(self, "@current_query_options"));
491491
(void)RB_GC_GUARD(current);
492492
Check_Type(current, T_HASH);
493-
resultObj = rb_mysql_result_to_obj(self, wrapper->encoding, current, result, NULL);
493+
resultObj = rb_mysql_result_to_obj(self, wrapper->encoding, current, result, Qnil);
494494

495495
return resultObj;
496496
}
@@ -1050,7 +1050,7 @@ static VALUE rb_mysql_client_store_result(VALUE self)
10501050
current = rb_hash_dup(rb_iv_get(self, "@current_query_options"));
10511051
(void)RB_GC_GUARD(current);
10521052
Check_Type(current, T_HASH);
1053-
resultObj = rb_mysql_result_to_obj(self, wrapper->encoding, current, result, NULL);
1053+
resultObj = rb_mysql_result_to_obj(self, wrapper->encoding, current, result, Qnil);
10541054

10551055
return resultObj;
10561056
}

ext/mysql2/mysql2_ext.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,8 @@ typedef unsigned int uint;
3939
#endif
4040

4141
#include <client.h>
42-
#include <result.h>
4342
#include <statement.h>
43+
#include <result.h>
4444
#include <infile.h>
4545

4646
#endif

ext/mysql2/result.c

Lines changed: 41 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -75,26 +75,37 @@ static VALUE sym_symbolize_keys, sym_as, sym_array, sym_database_timezone, sym_a
7575
sym_local, sym_utc, sym_cast_booleans, sym_cache_rows, sym_cast, sym_stream, sym_name;
7676
static ID intern_merge;
7777

78+
/* Mark any VALUEs that are only referenced in C, so the GC won't get them. */
7879
static void rb_mysql_result_mark(void * wrapper) {
7980
mysql2_result_wrapper * w = wrapper;
8081
if (w) {
8182
rb_gc_mark(w->fields);
8283
rb_gc_mark(w->rows);
8384
rb_gc_mark(w->encoding);
8485
rb_gc_mark(w->client);
86+
rb_gc_mark(w->statement);
8587
}
8688
}
8789

8890
/* this may be called manually or during GC */
8991
static void rb_mysql_result_free_result(mysql2_result_wrapper * wrapper) {
90-
unsigned int i;
9192
if (!wrapper) return;
9293

9394
if (wrapper->resultFreed != 1) {
94-
if (wrapper->stmt) {
95-
mysql_stmt_free_result(wrapper->stmt);
95+
if (wrapper->stmt_wrapper) {
96+
mysql_stmt_free_result(wrapper->stmt_wrapper->stmt);
97+
98+
/* MySQL BUG? If the statement handle was previously used, and so
99+
* mysql_stmt_bind_result was called, and if that result set and bind buffers were freed,
100+
* MySQL still thinks the result set buffer is available and will prefetch the
101+
* first result in mysql_stmt_execute. This will corrupt or crash the program.
102+
* By setting bind_result_done back to 0, we make MySQL think that a result set
103+
* has never been bound to this statement handle before to prevent the prefetch.
104+
*/
105+
wrapper->stmt_wrapper->stmt->bind_result_done = 0;
96106

97107
if (wrapper->result_buffers) {
108+
unsigned int i;
98109
for (i = 0; i < wrapper->numberOfFields; i++) {
99110
if (wrapper->result_buffers[i].buffer) {
100111
xfree(wrapper->result_buffers[i].buffer);
@@ -105,8 +116,11 @@ static void rb_mysql_result_free_result(mysql2_result_wrapper * wrapper) {
105116
xfree(wrapper->error);
106117
xfree(wrapper->length);
107118
}
119+
/* Clue that the next statement execute will need to allocate a new result buffer. */
120+
wrapper->result_buffers = NULL;
108121
}
109122
/* FIXME: this may call flush_use_result, which can hit the socket */
123+
/* For prepared statements, wrapper->result is the result metadata */
110124
mysql_free_result(wrapper->result);
111125
wrapper->resultFreed = 1;
112126
}
@@ -122,6 +136,10 @@ static void rb_mysql_result_free(void *ptr) {
122136
decr_mysql2_client(wrapper->client_wrapper);
123137
}
124138

139+
if (wrapper->statement != Qnil) {
140+
decr_mysql2_stmt(wrapper->stmt_wrapper);
141+
}
142+
125143
xfree(wrapper);
126144
}
127145

@@ -338,23 +356,23 @@ static VALUE rb_mysql_result_fetch_row_stmt(VALUE self, MYSQL_FIELD * fields, co
338356
rb_mysql_result_alloc_result_buffers(self, fields);
339357
}
340358

341-
if(mysql_stmt_bind_result(wrapper->stmt, wrapper->result_buffers)) {
342-
rb_raise_mysql2_stmt_error2(wrapper->stmt
359+
if (mysql_stmt_bind_result(wrapper->stmt_wrapper->stmt, wrapper->result_buffers)) {
360+
rb_raise_mysql2_stmt_error2(wrapper->stmt_wrapper->stmt
343361
#ifdef HAVE_RUBY_ENCODING_H
344362
, conn_enc
345363
#endif
346364
);
347365
}
348366

349367
{
350-
switch((uintptr_t)rb_thread_call_without_gvl(nogvl_stmt_fetch, wrapper->stmt, RUBY_UBF_IO, 0)) {
368+
switch((uintptr_t)rb_thread_call_without_gvl(nogvl_stmt_fetch, wrapper->stmt_wrapper->stmt, RUBY_UBF_IO, 0)) {
351369
case 0:
352370
/* success */
353371
break;
354372

355373
case 1:
356374
/* error */
357-
rb_raise_mysql2_stmt_error2(wrapper->stmt
375+
rb_raise_mysql2_stmt_error2(wrapper->stmt_wrapper->stmt
358376
#ifdef HAVE_RUBY_ENCODING_H
359377
, conn_enc
360378
#endif
@@ -870,11 +888,11 @@ static VALUE rb_mysql_result_each(int argc, VALUE * argv, VALUE self) {
870888
rb_warn(":cache_rows is ignored if :stream is true");
871889
}
872890

873-
if (wrapper->stmt && !cacheRows && !wrapper->is_streaming) {
891+
if (wrapper->stmt_wrapper && !cacheRows && !wrapper->is_streaming) {
874892
rb_warn(":cache_rows is forced for prepared statements (if not streaming)");
875893
}
876894

877-
if (wrapper->stmt && !cast) {
895+
if (wrapper->stmt_wrapper && !cast) {
878896
rb_warn(":cast is forced for prepared statements");
879897
}
880898

@@ -900,7 +918,7 @@ static VALUE rb_mysql_result_each(int argc, VALUE * argv, VALUE self) {
900918
}
901919

902920
if (wrapper->lastRowProcessed == 0 && !wrapper->is_streaming) {
903-
wrapper->numberOfRows = wrapper->stmt ? mysql_stmt_num_rows(wrapper->stmt) : mysql_num_rows(wrapper->result);
921+
wrapper->numberOfRows = wrapper->stmt_wrapper ? mysql_stmt_num_rows(wrapper->stmt_wrapper->stmt) : mysql_num_rows(wrapper->result);
904922
if (wrapper->numberOfRows == 0) {
905923
wrapper->rows = rb_ary_new();
906924
return wrapper->rows;
@@ -918,7 +936,7 @@ static VALUE rb_mysql_result_each(int argc, VALUE * argv, VALUE self) {
918936
args.app_timezone = app_timezone;
919937
args.block_given = block;
920938

921-
if (wrapper->stmt) {
939+
if (wrapper->stmt_wrapper) {
922940
fetch_row_func = rb_mysql_result_fetch_row_stmt;
923941
} else {
924942
fetch_row_func = rb_mysql_result_fetch_row;
@@ -940,16 +958,16 @@ static VALUE rb_mysql_result_count(VALUE self) {
940958
return LONG2NUM(RARRAY_LEN(wrapper->rows));
941959
} else {
942960
/* MySQL returns an unsigned 64-bit long here */
943-
if(wrapper->stmt) {
944-
return ULL2NUM(mysql_stmt_num_rows(wrapper->stmt));
961+
if (wrapper->stmt_wrapper) {
962+
return ULL2NUM(mysql_stmt_num_rows(wrapper->stmt_wrapper->stmt));
945963
} else {
946964
return ULL2NUM(mysql_num_rows(wrapper->result));
947965
}
948966
}
949967
}
950968

951969
/* Mysql2::Result */
952-
VALUE rb_mysql_result_to_obj(VALUE client, VALUE encoding, VALUE options, MYSQL_RES *r, MYSQL_STMT * s) {
970+
VALUE rb_mysql_result_to_obj(VALUE client, VALUE encoding, VALUE options, MYSQL_RES *r, VALUE statement) {
953971
VALUE obj;
954972
mysql2_result_wrapper * wrapper;
955973

@@ -966,12 +984,20 @@ VALUE rb_mysql_result_to_obj(VALUE client, VALUE encoding, VALUE options, MYSQL_
966984
wrapper->client = client;
967985
wrapper->client_wrapper = DATA_PTR(client);
968986
wrapper->client_wrapper->refcount++;
969-
wrapper->stmt = s;
970987
wrapper->result_buffers = NULL;
971988
wrapper->is_null = NULL;
972989
wrapper->error = NULL;
973990
wrapper->length = NULL;
974991

992+
/* Keep a handle to the Statement to ensure it doesn't get garbage collected first */
993+
wrapper->statement = statement;
994+
if (statement != Qnil) {
995+
wrapper->stmt_wrapper = DATA_PTR(statement);
996+
wrapper->stmt_wrapper->refcount++;
997+
} else {
998+
wrapper->stmt_wrapper = NULL;
999+
}
1000+
9751001
rb_obj_call_init(obj, 0, NULL);
9761002
rb_iv_set(obj, "@query_options", options);
9771003

ext/mysql2/result.h

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,22 @@
22
#define MYSQL2_RESULT_H
33

44
void init_mysql2_result();
5-
VALUE rb_mysql_result_to_obj(VALUE client, VALUE encoding, VALUE options, MYSQL_RES *r, MYSQL_STMT * s);
5+
VALUE rb_mysql_result_to_obj(VALUE client, VALUE encoding, VALUE options, MYSQL_RES *r, VALUE statement);
66

77
typedef struct {
88
VALUE fields;
99
VALUE rows;
1010
VALUE client;
1111
VALUE encoding;
12+
VALUE statement;
1213
unsigned int numberOfFields;
1314
unsigned long numberOfRows;
1415
unsigned long lastRowProcessed;
1516
char is_streaming;
1617
char streamingComplete;
1718
char resultFreed;
1819
MYSQL_RES *result;
19-
MYSQL_STMT *stmt;
20+
mysql_stmt_wrapper *stmt_wrapper;
2021
mysql_client_wrapper *client_wrapper;
2122
/* statement result bind buffers */
2223
MYSQL_BIND *result_buffers;

ext/mysql2/statement.c

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,23 @@ static VALUE intern_usec, intern_sec, intern_min, intern_hour, intern_day, inter
1212

1313
static void rb_mysql_stmt_mark(void * ptr) {
1414
mysql_stmt_wrapper* stmt_wrapper = (mysql_stmt_wrapper *)ptr;
15-
if(! stmt_wrapper) return;
15+
if (!stmt_wrapper) return;
1616

1717
rb_gc_mark(stmt_wrapper->client);
1818
}
1919

2020
static void rb_mysql_stmt_free(void * ptr) {
2121
mysql_stmt_wrapper* stmt_wrapper = (mysql_stmt_wrapper *)ptr;
22+
decr_mysql2_stmt(stmt_wrapper);
23+
}
2224

23-
mysql_stmt_close(stmt_wrapper->stmt);
25+
void decr_mysql2_stmt(mysql_stmt_wrapper *stmt_wrapper) {
26+
stmt_wrapper->refcount--;
2427

25-
xfree(ptr);
28+
if (stmt_wrapper->refcount == 0) {
29+
mysql_stmt_close(stmt_wrapper->stmt);
30+
xfree(stmt_wrapper);
31+
}
2632
}
2733

2834
VALUE rb_raise_mysql2_stmt_error2(MYSQL_STMT *stmt
@@ -103,6 +109,7 @@ VALUE rb_mysql_stmt_new(VALUE rb_client, VALUE sql) {
103109
rb_stmt = Data_Make_Struct(cMysql2Statement, mysql_stmt_wrapper, rb_mysql_stmt_mark, rb_mysql_stmt_free, stmt_wrapper);
104110
{
105111
stmt_wrapper->client = rb_client;
112+
stmt_wrapper->refcount = 1;
106113
stmt_wrapper->stmt = NULL;
107114
}
108115

@@ -169,7 +176,7 @@ static VALUE field_count(VALUE self) {
169176
static void *nogvl_execute(void *ptr) {
170177
MYSQL_STMT *stmt = ptr;
171178

172-
if(mysql_stmt_execute(stmt)) {
179+
if (mysql_stmt_execute(stmt)) {
173180
return (void*)Qfalse;
174181
} else {
175182
return (void*)Qtrue;
@@ -179,7 +186,7 @@ static void *nogvl_execute(void *ptr) {
179186
static void *nogvl_stmt_store_result(void *ptr) {
180187
MYSQL_STMT *stmt = ptr;
181188

182-
if(mysql_stmt_store_result(stmt)) {
189+
if (mysql_stmt_store_result(stmt)) {
183190
return (void *)Qfalse;
184191
} else {
185192
return (void *)Qtrue;
@@ -340,8 +347,8 @@ static VALUE execute(int argc, VALUE *argv, VALUE self) {
340347
FREE_BINDS;
341348

342349
metadata = mysql_stmt_result_metadata(stmt);
343-
if(metadata == NULL) {
344-
if(mysql_stmt_errno(stmt) != 0) {
350+
if (metadata == NULL) {
351+
if (mysql_stmt_errno(stmt) != 0) {
345352
// either CR_OUT_OF_MEMORY or CR_UNKNOWN_ERROR. both fatal.
346353

347354
MARK_CONN_INACTIVE(stmt_wrapper->client);
@@ -359,12 +366,13 @@ static VALUE execute(int argc, VALUE *argv, VALUE self) {
359366
if (!is_streaming) {
360367
// recieve the whole result set from the server
361368
if (rb_thread_call_without_gvl(nogvl_stmt_store_result, stmt, RUBY_UBF_IO, 0) == Qfalse) {
369+
mysql_free_result(metadata);
362370
rb_raise_mysql2_stmt_error(self);
363371
}
364372
MARK_CONN_INACTIVE(stmt_wrapper->client);
365373
}
366374

367-
resultObj = rb_mysql_result_to_obj(stmt_wrapper->client, wrapper->encoding, current, metadata, stmt);
375+
resultObj = rb_mysql_result_to_obj(stmt_wrapper->client, wrapper->encoding, current, metadata, self);
368376

369377
if (!is_streaming) {
370378
// cache all result

ext/mysql2/statement.h

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@
33

44
extern VALUE cMysql2Statement;
55

6-
void init_mysql2_statement();
7-
86
typedef struct {
97
VALUE client;
108
MYSQL_STMT *stmt;
9+
int refcount;
1110
} mysql_stmt_wrapper;
1211

12+
void init_mysql2_statement();
13+
void decr_mysql2_stmt(mysql_stmt_wrapper *stmt_wrapper);
14+
1315
VALUE rb_mysql_stmt_new(VALUE rb_client, VALUE sql);
1416
VALUE rb_raise_mysql2_stmt_error2(MYSQL_STMT *stmt
1517
#ifdef HAVE_RUBY_ENCODING_H

spec/mysql2/statement_spec.rb

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,22 @@
7070
@client.query 'DROP TABLE IF EXISTS mysql2_stmt_q'
7171
end
7272

73+
it "should be reusable 1000 times" do
74+
statement = @client.prepare 'SELECT 1'
75+
1000.times do
76+
result = statement.execute
77+
expect(result.to_a.length).to eq(1)
78+
end
79+
end
80+
81+
it "should be reusable 10000 times" do
82+
statement = @client.prepare 'SELECT 1'
83+
10000.times do
84+
result = statement.execute
85+
expect(result.to_a.length).to eq(1)
86+
end
87+
end
88+
7389
it "should select dates" do
7490
statement = @client.prepare 'SELECT NOW()'
7591
result = statement.execute

0 commit comments

Comments
 (0)