-
Notifications
You must be signed in to change notification settings - Fork 2.1k
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
ExecuteFetch
: error on multiple result sets
#14949
Conversation
Signed-off-by: Shlomi Noach <[email protected]>
Signed-off-by: Shlomi Noach <[email protected]>
Review ChecklistHello reviewers! 👋 Please follow this checklist when reviewing this Pull Request. General
Tests
Documentation
New flags
If a workflow is added or modified:
Backward compatibility
|
result, more, err = c.ExecuteFetchMulti(query, maxrows, wantfields) | ||
if more { | ||
return nil, vterrors.Errorf(vtrpc.Code_INTERNAL, "unexpected multiple results. Use ExecuteFetchMulti instead") | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Question: should we iterate and consume all results?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we must here, otherwise we leave the connection in an invalid state. And a subsequent query on the same connection would see the previous result here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@shlomi-noach are there other places in the code base where we pass in 0 incorrectly and do want to consume all the results?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure. ExecuteFetchMulti
potentially? But then, this bugs me, because we should be able to pass maxrows = 17
in any place, so why would the draining in ExecuteFetch
necessarily have to use -1
? And yet, it does, as per #14949 (comment). I'm not sure if this is again limited to stored procedure behavior. I don't think it is.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's not other explicit c.ReadQueryResult(0, ...)
call in the code, FWIW.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@harshit-gangal further edited the ExecuteFetch
/drain logic to fix potential leaks, and consolidated the draining logic. I think we should be good now.
…lt set errors Signed-off-by: Shlomi Noach <[email protected]>
As mostly expected, we get a bunch of CI errors. We should start tackling them one by one. |
Signed-off-by: Shlomi Noach <[email protected]>
Signed-off-by: Shlomi Noach <[email protected]>
Signed-off-by: Shlomi Noach <[email protected]>
Signed-off-by: Shlomi Noach <[email protected]>
Signed-off-by: Shlomi Noach <[email protected]>
Signed-off-by: Shlomi Noach <[email protected]>
Let me look into that. It's not supposed to happen now that we drain the connection. If anything, it shows that what we're trying to fix here is a nasty bug. |
I see the problem, and I have a simple patch to solve it, which is a bit of a cheat, and I'd like to understand this better. As @harshit-gangal suggested, the reason the test was failing was that the connection had result sets from the previous (multi result-set) test. Which surprised me, because the very essence of this PR is to drain all result sets in I found the the way The func (qre *QueryExecutor) drainResultSetOnConn(conn *connpool.Conn) error {
more := true
for more {
qr, err := conn.FetchNext(qre.ctx, int(qre.getSelectLimit()), true)
if err != nil {
return err
}
more = qr.IsMoreResultsExists()
}
return nil
} The func (c *Conn) ExecuteFetch(query string, maxrows int, wantfields bool) (result *sqltypes.Result, err error) {
var more bool
result, more, err = c.ExecuteFetchMulti(query, maxrows, wantfields)
if more {
// Multiple results are unexpected. Prioritize this "unexpected" error over whatever error we got from the first result.
err = errors.Join(ErrExecuteFetchMultipleResults, err)
}
// Even though we do not allow multiple result sets, we still prefer to drain them so as to clean the connection, as well as
// exhaust any further possible error.
for more {
var moreErr error
_, more, _, moreErr = c.ReadQueryResult(0, false)
if err != nil {
err = errors.Join(err, moreErr)
}
}
return result, err
} My solution, BTW, which is a bit of a cheat, is to close the connection when multipel result sets are found, like so: qr, err := qre.execDBConn(conn.Conn, sql, true)
if errors.UnwrappedIs(err, mysql.ErrExecuteFetchMultipleResults) {
conn.Close()
return nil, vterrors.New(vtrpcpb.Code_UNIMPLEMENTED, "Multi-Resultset not supported in stored procedure")
} It works, but I suspect there's better way to make this work. |
Found it! The difference was that _, more, _, moreErr = c.ReadQueryResult(0, false) Using |
Signed-off-by: Shlomi Noach <[email protected]>
Signed-off-by: Shlomi Noach <[email protected]>
Signed-off-by: Shlomi Noach <[email protected]>
Signed-off-by: Shlomi Noach <[email protected]>
Signed-off-by: Harshit Gangal <[email protected]>
… already exists (errno 1050) (sqlstate 42S01)' error Signed-off-by: Shlomi Noach <[email protected]>
Good to review! |
go/mysql/query.go
Outdated
// caring for any results. The function returns an error if any of the statements fail. | ||
// The function drains the query results of all statements, even if there's an error. | ||
func (c *Conn) ExecuteFetchMultiDrain(query string) (err error) { | ||
_, more, err := c.ExecuteFetchMulti(query, 0, false) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@shlomi-noach is the 0 here correct or could that result in a similar issue?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's correct, because the first query runs through ReadQueryResult
which drains the results. Then, ExecuteFetchMultiDrain
actively drains the results of the remaining queries. What;'s important here is that we don't do double-draining on the same result, which is what happened previously and was fixed both my my change and then by @harshit-gangal 's change on top.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can set -1
for visual consistency with other calls, too. That would be fine.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn’t 0 then much more efficient since we can avoid loading a whole bunch of data and allocating objects for it which we then drop here?
And does that imply we should be using 0 more in the case where we only care about if we get an error or not and not the actual result?
Dunno if we have many more of those cases in the code base though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm trying to make this work but keep getting the double-drain problem. @harshit-gangal is there a way for us to use 0
so that we don't read excessive rows into memory, and still drain correctly?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmmm actually I still have errors.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, this passes tests:
diff --git a/go/mysql/query.go b/go/mysql/query.go
index 18e9872752..d0f68ec35c 100644
--- a/go/mysql/query.go
+++ b/go/mysql/query.go
@@ -39,6 +39,13 @@ var (
ErrExecuteFetchMultipleResults = vterrors.Errorf(vtrpc.Code_INTERNAL, "unexpected multiple results. Use ExecuteFetchMulti instead.")
)
+const (
+ // Use as `maxrows` in `ExecuteFetch` and related functions, to indicate no rows should be fetched.
+ // This is different than specifying `0`, because `0` means "expect zero results", while this means
+ // "do not attempt to read any results into memory".
+ FETCH_NO_ROWS = math.MinInt
+)
+
//
// Client side methods.
//
@@ -322,7 +329,7 @@ func (c *Conn) ExecuteFetch(query string, maxrows int, wantfields bool) (result
// caring for any results. The function returns an error if any of the statements fail.
// The function drains the query results of all statements, even if there's an error.
func (c *Conn) ExecuteFetchMultiDrain(query string) (err error) {
- _, more, err := c.ExecuteFetchMulti(query, 0, false)
+ _, more, err := c.ExecuteFetchMulti(query, FETCH_NO_ROWS, false)
return c.drainMoreResults(more, err)
}
@@ -331,7 +338,7 @@ func (c *Conn) ExecuteFetchMultiDrain(query string) (err error) {
func (c *Conn) drainMoreResults(more bool, err error) error {
for more {
var moreErr error
- _, more, _, moreErr = c.ReadQueryResult(-1, false)
+ _, more, _, moreErr = c.ReadQueryResult(FETCH_NO_ROWS, false)
err = errors.Join(err, moreErr)
}
return err
@@ -451,6 +458,9 @@ func (c *Conn) ReadQueryResult(maxrows int, wantfields bool) (*sqltypes.Result,
if err != nil {
return nil, false, 0, sqlerror.NewSQLError(sqlerror.CRServerLost, sqlerror.SSUnknownSQLState, "%v", err)
}
+ if maxrows == FETCH_NO_ROWS {
+ return result, more, warnings, nil
+ }
if c.isEOFPacket(data) {
defer c.recycleReadPacket()
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Committed as a30c804, let's see how the entire CI reacts.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
CI looks happy. @harshit-gangal what do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks good to me
Signed-off-by: Shlomi Noach <[email protected]>
This PR is looking for two approvals! |
Actually, I'm adding a couple more unit tests here. Something I noticed. |
Signed-off-by: Shlomi Noach <[email protected]>
Signed-off-by: Shlomi Noach <[email protected]>
OK, I further modified the I added a couple unit tests that showed that |
Looking for another review. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks well thought out to me.
Signed-off-by: Vicent Marti <[email protected]>
@shlomi-noach I pushed a small commit to revert your https://cs.opensource.google/go/go/+/refs/tags/go1.22.0:src/errors/wrap.go;l=68-73 Also notably, if you change your unit test to use |
name = tcase.err.Error() | ||
} | ||
t.Run(name, func(t *testing.T) { | ||
is := UnwrappedIs(tcase.err, tcase.target) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@vmg if I replace is := UnwrappedIs
with is := errors.Is
then there actually is a test failure, but it's for testing whether nil
error is nil
, which returns true for errors.Is
and false for UnwrappedIs
. I'm perfectly happy with returning true
so the change is good.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, I did find that out. It seems like errors.Is(nil, nil) == true
is very sensible behavior.
Description
ExecuteFetch
now returns an error when faced with multiple result sets. It should only be used when a single result set is expected. See #14948The main change in this PR is in
go/mysql/query.go
. The rest of the PR is adaptation of many tests to the new logic, as well as fixing the logic of some tests, that were missing error responses due to the previous nature ofExecuteFetch
behavior.This PR should not be backported.
Related Issue(s)
Fixes #14948
Checklist
Deployment Notes