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

Bug Fix: Prepared Statements Bind Variables Nil Error #5441

Merged

Conversation

PrismaPhonic
Copy link
Contributor

This PR fixes the bind variables nil error first discovered in issue #5255.

The fix involved explicitly setting BindVars to an empty map, rather than looping through and setting each BindVar to nil in the defer function.

I also added a recover to catch any panics that might happen as a result of nil BindVars when using FormatBindVariables in Logf.

… Also added a recover to catch panics that occur inside Logf due to FormatBindVariables call potentially panicking.

Signed-off-by: Peter Farr <Peter@PrismaPhonic.com>
@PrismaPhonic PrismaPhonic force-pushed the fix/prepared-statements-bind-vars-nil branch from 3b38e60 to e83a02c Compare November 15, 2019 23:59
Signed-off-by: Peter Farr <Peter@PrismaPhonic.com>
Comment on lines -901 to -905
if prepare.BindVars != nil {
for k := range prepare.BindVars {
prepare.BindVars[k] = nil
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Though I agree that this is a cleaner and safer way of resetting PrepareData, I'm concerned that the fix might mask a deeper problem. Is it possible that there is a mismatch between ParamsCount and BindVars? Maybe we are being told to expect a certain number of bind variables (ParamsCount) but receiving a fewer number.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, so should we mask and report errors or something? or would rather have things immediately fail?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@deepthi That's a good point. Maybe the problem is that we are reusing stmtID values for different statements?

@PrismaPhonic Were you able to reproduce the problem with the reduced test case?

Copy link
Member

@enisoc enisoc Nov 18, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, although this is cleaner and safer than setting everything to nil, there is a performance cost to that. It was probably written this way to avoid allocating a new map on every query. This "fix" will put more load on the GC, which is something we shouldn't do in the hot path unless there's a very good reason.

The assumption written into this optimization seems to be that for a given stmtID, the set of bind var names will never change. Does anyone know of a valid situation in which that assumption is violated? If there is no such situation, then @deepthi is right that there is another bug here that we shouldn't cover up.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@deepthi will as far as I know we set the number of parameters when we check the :v prefix and send it back to the client. Therefore, if there was a mismatch I think the client might catch that.

@enisoc I dont know of a situation that will change the query and number of parameters for a given sstmtID.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK this finally makes sense. The clue about vtg1 in #5441 (comment) was the key to finding the root cause.

The vtg1 entry in the map is getting added by the normalizer after we pass the map to VTGate.Execute(). The way to reproduce this is to prepare a query that also has some raw values, such as the 'blah' in select foo from bar where name in (?,'blah'). The normalizer will replace the 'blah' with :vtg1 and add vtg1 to the bindvars map with value 'blah'.

After executing, we set all bindvar map values to nil without deleting them, which would have been fine if the normalizer had not mutated our map in-place to add vtg1. Instead, the next time we call VTGate.Execute(), we pass a bindvars map with vtg1 already present (and set to nil). The normalizer sees this and thinks you have a pre-existing bindvar called vtg1 so when it needs to generate a bindvar name for 'blah', it increments to vtg2 to avoid a collision. Now vtg1 remains set in the map with value nil.

The fix for that is what this PR is already doing: make a new map every time. The only suggestion I have then is to add a comment explaining why this is necessary:

// Allocate a new bindvar map every time since VTGate.Execute() mutates it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added the comment with 4411cda.

@enisoc
Copy link
Member

enisoc commented Nov 18, 2019

I noticed something weird about the example error message in #5255:

vtg1: bind variable is nil

As far as I can tell, the bind vars generated by the prepared statement code should remain v# (e.g. v1). A bind var called vtg1 should, if I'm reading the code correctly, only be possible if the original query contained a raw value that wasn't passed as a bind var to begin with. Those raw values get converted to vtg# bind vars by the normalizer. However, the normalizer seems to leave existing bind vars with their existing names.

Does anyone have any other example error messages for this case to compare?

go/mysql/conn.go Outdated
prepare.BindVars[k] = nil
}
}
paramsCount := len(prepare.BindVars)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should just use prepare.ParamsCount. The value of len(prepare.BindVars) could actually be greater than prepare.ParamsCount when there are raw values in the query, which get converted into additional bindvars by the normalizer.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call out. Will adjust after the party tonight. Thanks!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adjusted with 0da79a8.

Signed-off-by: Peter Farr <Peter@PrismaPhonic.com>
@PrismaPhonic PrismaPhonic force-pushed the fix/prepared-statements-bind-vars-nil branch from af722d8 to 0da79a8 Compare November 19, 2019 09:26
Signed-off-by: Peter Farr <Peter@PrismaPhonic.com>
Copy link
Contributor

@saifalharthi saifalharthi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I took a deeper look on this and it looks good to me!

@@ -125,6 +127,15 @@ func (stats *LogStats) Logf(w io.Writer, params url.Values) error {
return nil
}

// FormatBindVariables call might panic so we're going to catch it here and print out the stack trace to help us
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't an uncaught panic already print a stack trace? Or is the problem that someone else has been recovering the panic somewhere else in the call stack?

Copy link
Contributor Author

@PrismaPhonic PrismaPhonic Nov 19, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right that an uncaught panic already prints a stack trace. @sougou had asked me offline to use recover() to handle the place where the panic was occurring as an extra safety net. I was hoping he would comment here on the PR to clarify the full extent of what he wanted to achieve with the recover(). I've yet to have an opportunity to sync up with him about this part. Perhaps he meant without the explicit panic. @sougou?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I heard that @sougou can be found at the PlanetScale booth at KubeCon. You should try to find him there. :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Chatted with him. He meant without an explicit panic and deepthi clarified that vtgate by design should never panic. Correcting now

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adjusted c4270d2. PTAL

Signed-off-by: Peter Farr <Peter@PrismaPhonic.com>
@PrismaPhonic PrismaPhonic force-pushed the fix/prepared-statements-bind-vars-nil branch from fe7c867 to c4270d2 Compare November 20, 2019 19:57
Copy link
Member

@enisoc enisoc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@enisoc enisoc merged commit f7205b8 into vitessio:master Nov 22, 2019
@enisoc enisoc deleted the fix/prepared-statements-bind-vars-nil branch November 22, 2019 23:47
@deepthi
Copy link
Member

deepthi commented Nov 23, 2019

nice work @PrismaPhonic!

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.

5 participants