-
Notifications
You must be signed in to change notification settings - Fork 2.3k
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
Add support for context.Context #608
Add support for context.Context #608
Conversation
The approach looks awesomely clean! Please make entries to the AUTHORS file for all persons that your code is based on and yourself. |
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.
My other concern is, that it introduces rather heavy synchronization at many places.
Do you see any potential to reduce the synchronization anywhere?
connection.go
Outdated
func (mc *mysqlConn) cleanup() { | ||
func (mc *mysqlConn) cleanup(err error) { | ||
if err == nil { | ||
panic("nil error") |
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.
This should probably be a bit more descriptive
connection.go
Outdated
err = mc.writeCommandPacket(comQuit) | ||
} | ||
|
||
mc.cleanup() | ||
mc.cleanup(errors.New("mysql: connection is closed")) |
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.
Might a user see this error?
If yes, please define it in errors.go
If it is completely internal, please define it at the top of the file.
connection.go
Outdated
"time" | ||
) | ||
|
||
//a copy of context.Context from Go 1.7 and later. |
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.
s/from/for/
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.
and start the comment with a space please
connection.go
Outdated
finished chan<- struct{} | ||
|
||
mu sync.Mutex // guards following fields | ||
closed error // set non-nil when conn is closed, before closech is closed |
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.
this field has type error
but is basically just used as a bool
/flag. Please just use a bool
instead.
connection.go
Outdated
mc.mu.Lock() | ||
mc.canceledErr = err | ||
mc.mu.Unlock() | ||
mc.cleanup(errors.New("mysql: query canceled")) |
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.
same as for the other error
rows.go
Outdated
@@ -64,12 +65,24 @@ func (rows *mysqlRows) Columns() []string { | |||
return columns | |||
} | |||
|
|||
func (rows *mysqlRows) setFinish(f func()) { |
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.
just set is directly. This is not Java
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.
This is implementation of setfinish
interface that defined in connection_go18.go.
Of course, I can rewrite it setting directly, but QueryContext
will be a little complex.
// in QueryContext
switch r := rows.(type) {
case mysqlRows:
r.finish = mc.finish
case binaryRows:
r.finish = mc.finish
case textRows:
r.finish = mc.finish
default:
mc.finish()
}
Now implementation is here.
// in QueryContext
if set, ok := rows.(setfinish); ok {
set.setFinish(mc.finish)
} else {
mc.finish()
}
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.
If we also consider the interface and function definition, we don't save anything here but introduce some extra overhead and complexity.
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.
mc.startWatch
and mc.finish()
must be paired, but mysqlRows
is used internally.
So, I usesetfinish
to prevent mc.finish()
from being called unexpectedly.
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 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.
Option 2
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. I'll cherry-pick shogo82148/mysql@9b979f2.
Thanks. I added myself and the other persons. |
- s/from/for/ - and start the comment with a space please
The benchmark result (EC2 c4.large and RDS Aurora db.r3.xlarge) $ ~/go/bin/benchcmp old.txt new.txt
benchmark old ns/op new ns/op delta
BenchmarkQueryContext/1-2 102848 103137 +0.28%
BenchmarkQueryContext/2-2 54461 56205 +3.20%
BenchmarkQueryContext/3-2 39958 40097 +0.35%
BenchmarkQueryContext/4-2 31477 32254 +2.47%
BenchmarkExecContext/1-2 103821 103121 -0.67%
BenchmarkExecContext/2-2 55591 55437 -0.28%
BenchmarkExecContext/3-2 39455 40441 +2.50%
BenchmarkExecContext/4-2 32416 32988 +1.76%
BenchmarkQuery-2 26059 30051 +15.32%
BenchmarkExec-2 99030 102532 +3.54%
BenchmarkRoundtripTxt-2 343523 337323 -1.80%
BenchmarkRoundtripBin-2 339690 342116 +0.71%
BenchmarkInterpolation-2 833 837 +0.48%
BenchmarkParseDSN-2 9254 9444 +2.05%
benchmark old allocs new allocs delta
BenchmarkQueryContext/1-2 21 22 +4.76%
BenchmarkQueryContext/2-2 21 22 +4.76%
BenchmarkQueryContext/3-2 21 22 +4.76%
BenchmarkQueryContext/4-2 21 22 +4.76%
BenchmarkExecContext/1-2 21 22 +4.76%
BenchmarkExecContext/2-2 21 22 +4.76%
BenchmarkExecContext/3-2 21 22 +4.76%
BenchmarkExecContext/4-2 21 22 +4.76%
BenchmarkQuery-2 22 23 +4.55%
BenchmarkExec-2 3 3 +0.00%
BenchmarkRoundtripTxt-2 15 16 +6.67%
BenchmarkRoundtripBin-2 17 18 +5.88%
BenchmarkInterpolation-2 1 1 +0.00%
BenchmarkParseDSN-2 63 63 +0.00%
benchmark old bytes new bytes delta
BenchmarkQueryContext/1-2 697 731 +4.88%
BenchmarkQueryContext/2-2 696 728 +4.60%
BenchmarkQueryContext/3-2 696 728 +4.60%
BenchmarkQueryContext/4-2 696 728 +4.60%
BenchmarkExecContext/1-2 696 728 +4.60%
BenchmarkExecContext/2-2 696 728 +4.60%
BenchmarkExecContext/3-2 696 728 +4.60%
BenchmarkExecContext/4-2 696 728 +4.60%
BenchmarkQuery-2 713 745 +4.49%
BenchmarkExec-2 67 72 +7.46%
BenchmarkRoundtripTxt-2 15907 15940 +0.21%
BenchmarkRoundtripBin-2 663 695 +4.83%
BenchmarkInterpolation-2 176 176 +0.00%
BenchmarkParseDSN-2 6896 6896 +0.00% |
connection.go
Outdated
Deadline() (deadline time.Time, ok bool) | ||
Done() <-chan struct{} | ||
Err() error | ||
Value(key interface{}) interface{} |
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.
If method Value is not used, you can drop it. It is not necessary to describe all methods of interface context.Context.
connection.go
Outdated
finished chan<- struct{} | ||
|
||
mu sync.Mutex // guards following fields | ||
closed bool // set true when conn is closed, before closech is closed |
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.
IMHO: it will be better to use atomic variable for closed, because this field is used often than canceledError. I believe it will be faster than acquire lock for each time.
in this case mu will guard only for canceledErr field.
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.
Very good point, but Go currently doesn't have any atomic bool type in the standard library.
I would send another PR with an implementation based e.g. on uint32 to replace it after this is merged.
connection_go18.go
Outdated
"errors" | ||
) | ||
|
||
type setfinish interface { |
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.
Maybe finalizer
?
type finalizer interface {
finalize(f func())
}
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.
-1
because setFinish
doesn't finalize, but the function f
does.
Another suggestion.
type setterFinalizer interface {
setFinalizer(finalizer func())
}
Are there other better names?
rows.go
Outdated
@@ -167,7 +186,10 @@ func (rows *textRows) NextResultSet() (err error) { | |||
|
|||
func (rows *textRows) Next(dest []driver.Value) error { | |||
if mc := rows.mc; mc != nil { | |||
if mc.netConn == nil { | |||
if mc.isBroken() { |
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.
What about moving this logic into method of connection.
if err := mc.error(); err != nil {
return err
}
where mc.error is
if mc.isBroken() {
if err := mc.canceled(); err != nil {
return err
}
return ErrInvalidConn
}
return nil
} | ||
if err := rows.Err(); err != context.Canceled { | ||
dbt.Errorf("expected context.Canceled, got %v", err) | ||
} |
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.
IMHO: it also be good to add check that there is no hanged gorutines. the number of gorutines, before QueryContext, should be less or equal number of gorutines after closing connection.
When benchmarking, local MySQL is better than remote one, because network latency hides real performance. |
I'm +1 for general design. I'm looking more carefully about race condition. |
And add a section to the README please (" |
If possible, please also move the support for read-only transactions to a separate PR. This needs more consideration, as it is not supported by all servers. |
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.
Thanks for doing this work!
} | ||
|
||
func (mc *mysqlConn) startWatcher() { | ||
watcher := make(chan mysqlContext, 1) |
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.
You should create watcher
and finished
before starting this goroutine. If you initialize them here, then there's technically a race between this goroutine and other methods that use those channels.
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 technically a race between this goroutine and other methods that use those channels.
Yes, you are right.
But the watcher gorotuine starts in the end of startWatcher
function, and watcher
and finished
are initialized before starting the watcher gorotuine.
so I think that there is no problems.
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.
Ah, correct again. I misread this and thought that startWatcher
was called as go mc.startWatcher()
for some reason.
connection_go18.go
Outdated
} | ||
|
||
func (mc *mysqlConn) watchCancel(ctx context.Context) error { | ||
select { |
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.
Maybe check if ctx.Done() == nil
and if so, bail early? Seems like it would be a fairly common case and would allow you to save the overhead of channel synchronization and the overhead of defer mc.finish
, which you'll have to do only under the same condition.
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.
Thanks. I fixed it.
My MEMO:
Begin()
uses ctx.Done() == context.Background().Done()
for same reason.
https://github.com/golang/go/blob/go1.8.3/src/database/sql/ctxutil.go#L110
I checked which one is better.
$ ag -Q '.Done() == nil'
src/context/context.go
243: if parent.Done() == nil {
src/net/cgo_unix.go
81: if ctx.Done() == nil {
208: if ctx.Done() == nil {
223: if ctx.Done() == nil {
263: if ctx.Done() == nil {
src/net/http/h2_bundle.go
6414: if req.Cancel == nil && ctx.Done() == nil {
$ ag -Q '.Done() == context.Background()'
src/database/sql/ctxutil.go
110: if ctx.Done() == context.Background().Done() {
ctx.Done() == nil
wins.
} | ||
|
||
func (mc *mysqlConn) startWatcher() { | ||
watcher := make(chan mysqlContext, 1) |
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 also have some reservations about all connections sharing this same watcher goroutine.
On one hand, it's good that there's a single goroutine for doing this and we can avoid the overhead of goroutine creation every time we're running a query or whatever.
On the other, it means that only one connection can be watched at a time. And because the watcher
channel has a buffer size of 1, only two connections can be running queries at the same time. You could increase the buffer size of watcher
, but even if you do that only one connection will be watched for cancellation at a time.
For example, if you have an intentionally long-running query with a long timeout that's on the watched connection and you also have an unintentionally long-running query that sits in the watcher
buffer and will be canceled, the one sitting in the buffer has to wait until the first query finishes before it can realize that it's run too long and should be canceled.
I'm not sure what concrete suggestions to offer here, but I think it's worth mentioning.
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 is watcher per connection, not one watcher for all connections, isn't it?
Since mysql does not support multiplexing, only one query can be executed by the connection, so there is no problems IMHO.
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 is watcher per connection, not one watcher for all connections, isn't it?
Yes, it is.
The database/mysql package handles multiple connections, so mysqlConn
doesn't need to support multiplexing.
One watcher just watches one connection.
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.
You're right! My mistake. Disregard this comment.
Thank you for your comments! |
connection_go18.go
Outdated
}() | ||
} | ||
|
||
func namedValueToValue(named []driver.NamedValue) ([]driver.Value, error) { |
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.
This should be in utils_go18.go
The interface name suggested here #608 (comment) makes more sense to me and should be changed. Waiting for @methane, @bgaifullin, @edsrzf to approve it too now. |
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.
LGTM
@shogo82148 please rename the interface @methane, @bgaifullin please comment if you request any other changes or give your OK |
@julienschmidt I am waiting resolving for this 2 comments: Otherwise the PR seems OK to me. |
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.
@shogo82148 Good job!
I confirmed |
@methane Thanks a lot! @bgaifullin I see. I'll fix in a moment. |
@bgaifullin I resolved for the 2 comments, please check it. |
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.
@shogo82148 thank you.
LGTM
PR #612 proposed a small update to this PR by replacing the remaining lock with an atomic variable and introducing wrapper structs for those. |
* Add atomic wrappers for bool and error Improves #608 * Drop Go 1.2 and Go 1.3 support * "test" noCopy.Lock()
@shogo82148 would you mind looking at #858 ? |
Description
This pull request is based on @bgaifullin's work in https://github.com/bgaifullin/mysql/commit/645810cb2dad3528d2b4be2b8432a898df348bbe , @edsrzf's work in #586, and @oscarzhao's work in #551.
refer to #496
Versus other implementation
With support of cancelation
This pull request is based on @bgaifullin's work with support of cancelation.
More detailed error information
The users want to distinguish between cancellation error and network error.
For example, the following code should panic with
context.Canceled
.This behavior is similar to the net/http package.
No race condition
fixed the race condition of @bgaifullin's work (https://github.com/bgaifullin/mysql/commit/645810cb2dad3528d2b4be2b8432a898df348bbe#diff-7cd962f1d3aee9adf93d5c17b49f4530R98).
I've checked by my race condition checker for go-mysql-driver.
I referred to the implementation of the http package.
Support of Read Only TransactionSTART TRANSACTION READ ONLY
is supported from MySQL 5.7.https://dev.mysql.com/doc/refman/5.7/en/innodb-performance-ro-txn.html
EDITED: I'll moved it to a separate PR.
Checklist