Join GitHub today
GitHub is home to over 50 million developers working together to host and review code, manage projects, and build software together.
Sign upAdd 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. |
My other concern is, that it introduces rather heavy synchronization at many places. Do you see any potential to reduce the synchronization anywhere? |
func (mc *mysqlConn) cleanup() { | ||
func (mc *mysqlConn) cleanup(err error) { | ||
if err == nil { | ||
panic("nil error") |
julienschmidt
Jun 5, 2017
Member
This should probably be a bit more descriptive
This should probably be a bit more descriptive
err = mc.writeCommandPacket(comQuit) | ||
} | ||
|
||
mc.cleanup() | ||
mc.cleanup(errors.New("mysql: connection is closed")) |
julienschmidt
Jun 5, 2017
Member
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.
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.
"time" | ||
) | ||
|
||
//a copy of context.Context from Go 1.7 and later. |
julienschmidt
Jun 5, 2017
Member
s/from/for/
s/from/for/
julienschmidt
Jun 5, 2017
Member
and start the comment with a space please
and start the comment with a space please
finished chan<- struct{} | ||
|
||
mu sync.Mutex // guards following fields | ||
closed error // set non-nil when conn is closed, before closech is closed |
julienschmidt
Jun 5, 2017
Member
this field has type error
but is basically just used as a bool
/flag. Please just use a bool
instead.
this field has type error
but is basically just used as a bool
/flag. Please just use a bool
instead.
mc.mu.Lock() | ||
mc.canceledErr = err | ||
mc.mu.Unlock() | ||
mc.cleanup(errors.New("mysql: query canceled")) |
julienschmidt
Jun 5, 2017
Member
same as for the other error
same as for the other error
@@ -64,12 +65,24 @@ func (rows *mysqlRows) Columns() []string { | |||
return columns | |||
} | |||
|
|||
func (rows *mysqlRows) setFinish(f func()) { |
julienschmidt
Jun 5, 2017
Member
just set is directly. This is not Java
just set is directly. This is not Java
shogo82148
Jun 5, 2017
Author
Contributor
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()
}
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()
}
julienschmidt
Jun 5, 2017
Member
If we also consider the interface and function definition, we don't save anything here but introduce some extra overhead and complexity.
If we also consider the interface and function definition, we don't save anything here but introduce some extra overhead and complexity.
shogo82148
Jun 6, 2017
Author
Contributor
mc.startWatch
and mc.finish()
must be paired, but mysqlRows
is used internally.
So, I usesetfinish
to prevent mc.finish()
from being called unexpectedly.
mc.startWatch
and mc.finish()
must be paired, but mysqlRows
is used internally.
So, I usesetfinish
to prevent mc.finish()
from being called unexpectedly.
julienschmidt
Jun 6, 2017
Member
Option 2
Option 2
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% |
Deadline() (deadline time.Time, ok bool) | ||
Done() <-chan struct{} | ||
Err() error | ||
Value(key interface{}) interface{} |
bgaifullin
Jun 5, 2017
Contributor
If method Value is not used, you can drop it. It is not necessary to describe all methods of interface context.Context.
If method Value is not used, you can drop it. It is not necessary to describe all methods of interface context.Context.
finished chan<- struct{} | ||
|
||
mu sync.Mutex // guards following fields | ||
closed bool // set true when conn is closed, before closech is closed |
bgaifullin
Jun 5, 2017
Contributor
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.
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.
julienschmidt
Jun 5, 2017
•
Member
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.
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.
"errors" | ||
) | ||
|
||
type setfinish interface { |
bgaifullin
Jun 5, 2017
Contributor
Maybe finalizer
?
type finalizer interface {
finalize(f func())
}
Maybe finalizer
?
type finalizer interface {
finalize(f func())
}
shogo82148
Jun 6, 2017
Author
Contributor
-1
because setFinish
doesn't finalize, but the function f
does.
Another suggestion.
type setterFinalizer interface {
setFinalizer(finalizer func())
}
Are there other better names?
-1
because setFinish
doesn't finalize, but the function f
does.
Another suggestion.
type setterFinalizer interface {
setFinalizer(finalizer func())
}
Are there other better names?
@@ -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() { |
bgaifullin
Jun 5, 2017
Contributor
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
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) | ||
} |
bgaifullin
Jun 5, 2017
Contributor
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.
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. |
Thanks for doing this work! |
} | ||
|
||
func (mc *mysqlConn) startWatcher() { | ||
watcher := make(chan mysqlContext, 1) |
edsrzf
Jun 5, 2017
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.
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.
shogo82148
Jun 6, 2017
Author
Contributor
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'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.
edsrzf
Jun 6, 2017
Ah, correct again. I misread this and thought that startWatcher
was called as go mc.startWatcher()
for some reason.
Ah, correct again. I misread this and thought that startWatcher
was called as go mc.startWatcher()
for some reason.
} | ||
|
||
func (mc *mysqlConn) watchCancel(ctx context.Context) error { | ||
select { |
edsrzf
Jun 5, 2017
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.
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.
shogo82148
Jun 6, 2017
Author
Contributor
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.
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) |
edsrzf
Jun 5, 2017
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.
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.
bgaifullin
Jun 5, 2017
Contributor
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 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.
shogo82148
Jun 6, 2017
Author
Contributor
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 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.
edsrzf
Jun 6, 2017
You're right! My mistake. Disregard this comment.
You're right! My mistake. Disregard this comment.
Thank you for your comments! |
This comment has been minimized.
This comment has been minimized.
A user might face all these errors, right? If so, define them in |
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. |
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. |
@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. |
@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