-
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
ExecContext should return error if it is canceled during execution #1287
Comments
Yes. It is intended. After |
I understand that For example, in the following code, if ctx, cancel := context.WithCancel(context.Background())
tx, err := db.BeginTx(context.Background(), nil) // same with db.BeginTx(ctx, nil)
if err != nil {
fmt.Println(err)
return
}
go func() {
time.Sleep(time.Millisecond)
cancel()
}()
_, err = tx.ExecContext(ctx, `query`)
if err != nil {
fmt.Println(err)
tx.Rollback()
return
}
err = tx.Commit()
if err != nil {
fmt.Println(err)
tx.Rollback()
return
} |
Could you provide a reproducible example, instead of your expectation? |
For your hint, you can use |
Here is a sample code that can be reproduced. We assume that we have MySQL server running that is accessible on package main
import (
"context"
"database/sql"
"errors"
"log"
"sync"
"time"
_ "github.com/go-sql-driver/mysql"
)
func main() {
db, err := sql.Open("mysql", "root:@tcp(localhost:4446)/")
if err != nil {
log.Fatal(err)
}
if _, err = db.Exec(`CREATE DATABASE IF NOT EXISTS test_db;`); err != nil {
log.Fatal(err)
}
if err = db.Close(); err != nil {
log.Fatal(err)
}
db, err = sql.Open("mysql", "root:@tcp(localhost:4446)/test_db?multiStatements=true")
if err != nil {
log.Fatal(err)
}
defer db.Close()
if _, err = db.Exec(`
DROP TABLE IF EXISTS test_table;
CREATE TABLE test_table (
id BIGINT,
value BIGINT,
PRIMARY KEY (id)
) ENGINE = InnoDB COMMENT = 'test_table' DEFAULT CHARACTER SET utf8mb4;
`); err != nil {
log.Fatal(err)
}
wg := sync.WaitGroup{}
for i := 0; i < 10000; i++ {
ctx, cancel := context.WithCancel(context.Background())
tx, err := db.BeginTx(context.Background(), nil)
if err != nil {
log.Fatal(err)
}
_, err = tx.ExecContext(ctx, `UPDATE test_table SET value = 1`)
wg.Add(1)
go func() {
time.Sleep(1 * time.Millisecond) // adjust sleep time
cancel()
wg.Done()
}()
_, err = tx.ExecContext(ctx, `UPDATE test_table SET value = 2`)
if err != nil && errors.Is(err, context.Canceled) {
log.Println("ExecContext Error ", i)
continue
}
if err = tx.Commit(); err != nil {
log.Fatal("Commit Error ", i, err)
}
wg.Wait()
}
} |
What is problem here? I think it is intended behavior. |
|
I don't think your "reproducer" don't reproduce the problem you are talking. Have you really confirmed that "ExecContext rarely does not get an error"? Or is it just your thought? |
When I replaced It demonstrates "rarely does not get an error" is not correct. |
Yes, I confirmed. Here is the result of executing the following code.
|
Isn't it just because 1ms is too short? Single UPDATE query is too fast to cancel. Use |
It's also understandable that running your SLEEP (4) will always result in an error. This is because the event reported this time only occurs when the context cacncels during defer () after the query result is returned. https://github.com/go-sql-driver/mysql/blob/v1.6.0/connection.go#L523 |
If context is canceled during defer finish () after mc.Exec is successful, tx.ExecContext will treat the connection as a Bad Connection without returning an error. |
Context may be cancelled anywhere. It may be cancelled during How the problem you are reporting is different from that? |
OK, now I understand it.
That is definitely different from ctx is cancelled during In regular (e.g. autocommit) case, I don't want to throw away results when Exec() succeeded but connection is marked but by sad timing. Query is executed surely. On the other hand, in the transactional case, you are right. Although we need to care about Commit error, I want to avoid it as possible. |
By the way, I don't recommend to use ExecContext at all. MySQL don't provide a mechanism to cancel query safely. That's why we just close the TCP connection. It is very bad. |
Yes, that's right. For example, if you create a transaction for multiple destinations, you want to avoid Commit failure even though ExecContext succeeds.
I think so, but the reason I use ExecContext is because I want to link requests and queries when tracing an application. |
Oh. It would be better to add an option to omit context cancel support for such use cases. |
I don't need a cacnel while executing a query, but I thought it would be better to check if it was cancel before executing the query. |
The MySQL driver does it in https://github.com/go-sql-driver/mysql/blob/master/connection.go#L581 |
@KoteiIto I find bizarre to use a separate context.Context for the transaction ( What about creating a child context that doesn't propagate the cancelable property ( type withoutCancel struct {
context.Context
}
func (ctx withoutCancel) Done() <-chan struct{} {
return nil
}
func (ctx withoutCancel) Err() (err error) {
err = ctx.Context.Err()
if err == context.Canceled {
err = nil
}
return
} |
Issue description
mysqlConn.ExecContext
usesmysqlConn.watchCancel
to decide if thectx
passed as an argument is canceled, so if thectx
is canceled before the decision, it returns an error. However,mysqlConn.ExecContext
does not return an error even if thectx
is cancelled during or aftermysqlConn.Exec
execution, or duringmysqlConn.finish
execution indefer
. Is this the intended behavior?Configuration
Driver version (or git SHA):
Go version: 1.17.2
Server version: E.g. MySQL 5.7
Server OS: amazonlinux:latest(Docker Image)
The text was updated successfully, but these errors were encountered: