Skip to content

Commit 515ee31

Browse files
committed
Prevent command out of sync errors with Prepared Statements
This commit fixes two error scenarios. The first is to avoid GC runs between `mysql_stmt_execute` and `mysql_stmt_store_result`. This fixes a regression caused by #912 due to calling `rb_funcall` between `mysql_stmt_execute` and `mysql_stmt_store_result`. The error in this case is: Commands out of sync; you can't run this command now Thanks to @kamipo for diagnosing the problem and drafting the first PR. The second problem is that in streaming mode, rows are returned to Ruby space one at a time, so garbage collection will naturally occur at any time. By requesting a cursor into the result set, other MySQL commands can be sent on the wire between row fetches. The error in this case is: Row retrieval was canceled by mysql_stmt_close Fixes #956, updates #957.
1 parent bf227ac commit 515ee31

File tree

1 file changed

+32
-11
lines changed

1 file changed

+32
-11
lines changed

ext/mysql2/statement.c

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,38 @@ static VALUE rb_mysql_stmt_execute(int argc, VALUE *argv, VALUE self) {
403403
}
404404
}
405405

406+
// Duplicate the options hash, merge! extra opts, put the copy into the Result object
407+
current = rb_hash_dup(rb_iv_get(stmt_wrapper->client, "@query_options"));
408+
(void)RB_GC_GUARD(current);
409+
Check_Type(current, T_HASH);
410+
411+
// Merge in hash opts/keyword arguments
412+
if (!NIL_P(opts)) {
413+
rb_funcall(current, intern_merge_bang, 1, opts);
414+
}
415+
416+
is_streaming = (Qtrue == rb_hash_aref(current, sym_stream));
417+
418+
// From stmt_execute to mysql_stmt_result_metadata to stmt_store_result, no
419+
// Ruby API calls are allowed so that GC is not invoked. If the connection is
420+
// in results-streaming-mode for Statement A, and in the middle Statement B
421+
// gets garbage collected, a message will be sent to the server notifying it
422+
// to release Statement B, resulting in the following error:
423+
// Commands out of sync; you can't run this command now
424+
//
425+
// In streaming mode, statement execute must return a cursor because we
426+
// cannot prevent other Statement objects from being garbage collected
427+
// between fetches of each row of the result set. The following error
428+
// occurs if cursor mode is not set:
429+
// Row retrieval was canceled by mysql_stmt_close
430+
431+
if (is_streaming) {
432+
unsigned long type = CURSOR_TYPE_READ_ONLY;
433+
if (mysql_stmt_attr_set(stmt, STMT_ATTR_CURSOR_TYPE, &type)) {
434+
rb_raise(cMysql2Error, "Unable to stream prepared statement, could not set CURSOR_TYPE_READ_ONLY");
435+
}
436+
}
437+
406438
if ((VALUE)rb_thread_call_without_gvl(nogvl_stmt_execute, stmt, RUBY_UBF_IO, 0) == Qfalse) {
407439
FREE_BINDS;
408440
rb_raise_mysql2_stmt_error(stmt_wrapper);
@@ -421,17 +453,6 @@ static VALUE rb_mysql_stmt_execute(int argc, VALUE *argv, VALUE self) {
421453
return Qnil;
422454
}
423455

424-
// Duplicate the options hash, merge! extra opts, put the copy into the Result object
425-
current = rb_hash_dup(rb_iv_get(stmt_wrapper->client, "@query_options"));
426-
(void)RB_GC_GUARD(current);
427-
Check_Type(current, T_HASH);
428-
429-
// Merge in hash opts/keyword arguments
430-
if (!NIL_P(opts)) {
431-
rb_funcall(current, intern_merge_bang, 1, opts);
432-
}
433-
434-
is_streaming = (Qtrue == rb_hash_aref(current, sym_stream));
435456
if (!is_streaming) {
436457
// recieve the whole result set from the server
437458
if (mysql_stmt_store_result(stmt)) {

0 commit comments

Comments
 (0)