Skip to content

Commit ed9aa84

Browse files
authored
Prevent command out of sync errors with Prepared Statements (#958)
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 ed9aa84

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)