Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Avoid GC run between mysql_stmt_execute and mysql_stmt_store_result #957

Closed

Conversation

kamipo
Copy link
Contributor

@kamipo kamipo commented Apr 4, 2018

This fixes a regression caused by #912 due to calling rb_funcall
between mysql_stmt_execute and mysql_stmt_store_result, it will
cause mysql_stmt_close to be called in wrong order.

Fixes #956.

This fixes a regression caused by brianmario#912 due to calling `rb_funcall`
between `mysql_stmt_execute` and `mysql_stmt_store_result`, it will
cause `mysql_stmt_close` to be called in wrong order.

Fixes brianmario#956.
@sodabrew
Copy link
Collaborator

sodabrew commented Apr 4, 2018

Nice find! I am worried that this will be fragile, and some other call into Ruby space can still creep in here. I'm looking at whether there is a missing refcount.

@sodabrew
Copy link
Collaborator

sodabrew commented Apr 4, 2018

In result.c, rb_mysql_result_to_obj, there's a comment I left a while back:

 /* Keep a handle to the Statement to ensure it doesn't get garbage collected first */
 wrapper->statement = statement;
 if (statement != Qnil) {
   wrapper->stmt_wrapper = DATA_PTR(statement);
   wrapper->stmt_wrapper->refcount++;
 } else {
   wrapper->stmt_wrapper = NULL;
 }

I am wondering how the statement is possibly getting garbage collected at the point of this rb_funcall, if later down in the file, self is passed into rb_mysql_result_to_obj. I tried adding RB_GC_GUARD(self) at several location in rb_mysql_stmt_execute but that did not help.

Your solution avoids rb_funcall - but I read through rb_hash_aref and found that it is possible to call rb_funcall if the hash has a default value callback and the key is missing. This is admittedly not likely, but I was able to reproduce the break by adding this to the repro script:

connection_1.query_options.default_proc = ->(h, k) { puts "Hello #{k}" }

The output Hello stream prints a few times and then fails with Commands out of sync again.

I put together a different movement of code and was able to protect against this failure as well.

Bottom line, a GC run between mysql_stmt_execute and mysql_stmt_store_result is collecting the statement handle, and I don't understand why.

@sodabrew
Copy link
Collaborator

sodabrew commented Apr 4, 2018

I found a commit to the sqlite gem showing another case of a hard GC workaround: sparklemotion/sqlite3-ruby@380287a

@sodabrew
Copy link
Collaborator

sodabrew commented Apr 4, 2018

Aha, I get it now. The issue is that once a statement begins executing, any other command is invalid such as freeing a much older statement handle, which requires a notification from the client library to the server to release the handle, but isn't valid if the server is in results-streaming-mode.

@sodabrew
Copy link
Collaborator

sodabrew commented Apr 4, 2018

Which leads me to wonder... what about streaming results? Yep, that breaks, too! In the repro script:

result_1    = statement_1.execute(1, stream: true, cache_rows: false).to_a

Even with both of our proposed fixes:

outofsync.rb:17:in `each': Row retrieval was canceled by mysql_stmt_close() call (Mysql2::Error)

@sodabrew
Copy link
Collaborator

sodabrew commented Apr 4, 2018

According to the last comment on https://dev.mysql.com/doc/refman/5.7/en/commands-out-of-sync.html, and confirmed locally, setting a cursor for the statement execution allows other commands to be processed:

const unsigned long type = (unsigned long) CURSOR_TYPE_READ_ONLY;
mysql_stmt_attr_set (stmt, STMT_ATTR_CURSOR_TYPE, (void*) &type);

I'm looking at whether the cursor makes sense in all cases, or only in the streaming case.

Edit: seems to be fine adding the cursor in both cases, and it completely alleviates the GC problem.

@kamipo
Copy link
Contributor Author

kamipo commented Apr 5, 2018

Yeah, originally fixing the case of stream: true is out of target in my fix since I didn't know whether it is possible or not.
If possible that, probably it will solve both cases.

I'm closing this in favor of #958.

@kamipo kamipo closed this Apr 5, 2018
@kamipo kamipo deleted the avoid_gc_run_between_mysql_api_calls branch April 5, 2018 01:00
sodabrew added a commit to sodabrew/mysql2 that referenced this pull request Apr 7, 2018
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 brianmario#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 brianmario#956, updates brianmario#957.
sodabrew added a commit to sodabrew/mysql2 that referenced this pull request Apr 7, 2018
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 brianmario#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 brianmario#956, updates brianmario#957.
sodabrew added a commit to sodabrew/mysql2 that referenced this pull request Apr 7, 2018
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 brianmario#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 brianmario#956, updates brianmario#957.
sodabrew added a commit that referenced this pull request Apr 7, 2018
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Random failures using prepared statements
2 participants