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

Normalize context cancellation errors #1

Merged
merged 2 commits into from
Oct 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ func NewBindingPool(handle *sql.DB, options ...option) BindingConnectionPool {
}
func newPool(handle *sql.DB, config configuration) ConnectionPool {
var pool ConnectionPool = NewLibraryConnectionPoolAdapter(handle, config.txOptions)
pool = NewNormalizeContextCancellationConnectionPool(pool)

if config.splitStatement {
pool = NewSplitStatementConnectionPool(pool, config.parameterPrefix)
Expand Down
55 changes: 55 additions & 0 deletions normalize_context_cancellation_connection_pool.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package sqldb

import (
"context"
"fmt"
"strings"
)

type NormalizeContextCancellationConnectionPool struct {
inner ConnectionPool
}

func NewNormalizeContextCancellationConnectionPool(inner ConnectionPool) *NormalizeContextCancellationConnectionPool {
return &NormalizeContextCancellationConnectionPool{inner: inner}
}

func (this *NormalizeContextCancellationConnectionPool) Ping(ctx context.Context) error {
return this.normalizeContextCancellationError(this.inner.Ping(ctx))
}

func (this *NormalizeContextCancellationConnectionPool) BeginTransaction(ctx context.Context) (Transaction, error) {
if tx, err := this.inner.BeginTransaction(ctx); err == nil {
return NewStackTraceTransaction(tx), nil
} else {
return nil, this.normalizeContextCancellationError(err)
}
}

func (this *NormalizeContextCancellationConnectionPool) Close() error {
return this.normalizeContextCancellationError(this.inner.Close())
}

func (this *NormalizeContextCancellationConnectionPool) Execute(ctx context.Context, statement string, parameters ...interface{}) (uint64, error) {
affected, err := this.inner.Execute(ctx, statement, parameters...)
return affected, this.normalizeContextCancellationError(err)
}

func (this *NormalizeContextCancellationConnectionPool) Select(ctx context.Context, query string, parameters ...interface{}) (SelectResult, error) {
result, err := this.inner.Select(ctx, query, parameters...)
return result, this.normalizeContextCancellationError(err)
}

// TODO remove manual check of "use of closed network connection" with release of https://github.com/go-sql-driver/mysql/pull/1615
func (this *NormalizeContextCancellationConnectionPool) normalizeContextCancellationError(err error) error {
if err == nil {
return nil
}
if strings.Contains(err.Error(), "operation was canceled") {
return fmt.Errorf("%w: %w", context.Canceled, err)
}
if strings.Contains(err.Error(), "use of closed network connection") {
return fmt.Errorf("%w: %w", context.Canceled, err)
}
return err
}
206 changes: 206 additions & 0 deletions normalize_context_cancellation_connection_pool_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
package sqldb

import (
"context"
"errors"
"testing"

"github.com/smarty/assertions/should"
"github.com/smarty/gunit"
)

func TestNormalizeContextCancellationConnectionPoolFixture(t *testing.T) {
gunit.Run(new(NormalizeContextCancellationConnectionPoolFixture), t)
}

type NormalizeContextCancellationConnectionPoolFixture struct {
*gunit.Fixture

inner *FakeConnectionPool
adapter *NormalizeContextCancellationConnectionPool
}

func (this *NormalizeContextCancellationConnectionPoolFixture) Setup() {
this.inner = &FakeConnectionPool{}
this.adapter = NewNormalizeContextCancellationConnectionPool(this.inner)
}

func (this *NormalizeContextCancellationConnectionPoolFixture) TestPing_Successful() {
err := this.adapter.Ping(context.Background())

this.So(err, should.BeNil)
this.So(this.inner.pingCalls, should.Equal, 1)
}
func (this *NormalizeContextCancellationConnectionPoolFixture) TestPing_Failed() {
pingErr := errors.New("PING ERROR")
this.inner.pingError = pingErr

err := this.adapter.Ping(context.Background())

this.So(this.inner.pingCalls, should.Equal, 1)
this.So(err, should.Equal, pingErr)
}
func (this *NormalizeContextCancellationConnectionPoolFixture) TestPing_AdaptContextCancelled() {
this.inner.pingError = operationCanceledErr

err := this.adapter.Ping(context.Background())

this.So(this.inner.pingCalls, should.Equal, 1)
this.So(errors.Is(err, operationCanceledErr), should.BeTrue)
this.So(errors.Is(err, context.Canceled), should.BeTrue)
}

func (this *NormalizeContextCancellationConnectionPoolFixture) TestBeginTransaction_Successful() {
transaction := new(FakeTransaction)
this.inner.transaction = transaction

tx, err := this.adapter.BeginTransaction(context.Background())

this.So(err, should.BeNil)
this.So(this.inner.transactionCalls, should.Equal, 1)
this.So(tx, should.NotBeNil)
}
func (this *NormalizeContextCancellationConnectionPoolFixture) TestBeginTransaction_Failed() {
transactionErr := errors.New("BEGIN TRANSACTION ERROR")
this.inner.transactionError = transactionErr

tx, err := this.adapter.BeginTransaction(context.Background())

this.So(tx, should.BeNil)
this.So(err, should.Equal, transactionErr)
}
func (this *NormalizeContextCancellationConnectionPoolFixture) TestBeginTransaction_AdaptContextCancelled() {
this.inner.transactionError = operationCanceledErr

tx, err := this.adapter.BeginTransaction(context.Background())

this.So(tx, should.BeNil)
this.So(errors.Is(err, operationCanceledErr), should.BeTrue)
this.So(errors.Is(err, context.Canceled), should.BeTrue)
}

func (this *NormalizeContextCancellationConnectionPoolFixture) TestClose_Successful() {
err := this.adapter.Close()

this.So(err, should.BeNil)
this.So(this.inner.closeCalls, should.Equal, 1)
}
func (this *NormalizeContextCancellationConnectionPoolFixture) TestClose_Failed() {
closeErr := errors.New("CLOSE ERROR")
this.inner.closeError = closeErr

err := this.adapter.Close()

this.So(this.inner.closeCalls, should.Equal, 1)
this.So(err, should.Equal, closeErr)
}
func (this *NormalizeContextCancellationConnectionPoolFixture) TestClose_AdaptContextCancelled() {
this.inner.closeError = operationCanceledErr

err := this.adapter.Close()

this.So(this.inner.closeCalls, should.Equal, 1)
this.So(errors.Is(err, operationCanceledErr), should.BeTrue)
this.So(errors.Is(err, context.Canceled), should.BeTrue)
}

func (this *NormalizeContextCancellationConnectionPoolFixture) TestExecute_Successful() {
this.inner.executeResult = 42

result, err := this.adapter.Execute(context.Background(), "statement")

this.So(result, should.Equal, 42)
this.So(err, should.BeNil)
this.So(this.inner.executeCalls, should.Equal, 1)
this.So(this.inner.executeStatement, should.Equal, "statement")
}
func (this *NormalizeContextCancellationConnectionPoolFixture) TestExecute_Failed() {
this.inner.executeResult = 42
executeErr := errors.New("EXECUTE ERROR")
this.inner.executeError = executeErr

result, err := this.adapter.Execute(context.Background(), "statement")

this.So(result, should.Equal, 42)
this.So(err, should.Equal, executeErr)
this.So(this.inner.executeCalls, should.Equal, 1)
this.So(this.inner.executeStatement, should.Equal, "statement")
}
func (this *NormalizeContextCancellationConnectionPoolFixture) TestExecute_AdaptContextCancelled() {
this.inner.executeResult = 42
this.inner.executeError = operationCanceledErr

result, err := this.adapter.Execute(context.Background(), "statement")

this.So(result, should.Equal, 42)
this.So(this.inner.executeCalls, should.Equal, 1)
this.So(this.inner.executeStatement, should.Equal, "statement")
this.So(errors.Is(err, operationCanceledErr), should.BeTrue)
this.So(errors.Is(err, context.Canceled), should.BeTrue)
}

func (this *NormalizeContextCancellationConnectionPoolFixture) TestSelect_Successful() {
expectedResult := new(FakeSelectResult)
this.inner.selectResult = expectedResult

result, err := this.adapter.Select(context.Background(), "query", 1, 2, 3)

this.So(result, should.Equal, expectedResult)
this.So(err, should.BeNil)
this.So(this.inner.selectCalls, should.Equal, 1)
this.So(this.inner.selectStatement, should.Equal, "query")
this.So(this.inner.selectParameters, should.Equal, []any{1, 2, 3})
}
func (this *NormalizeContextCancellationConnectionPoolFixture) TestSelect_Failed() {
expectedResult := new(FakeSelectResult)
this.inner.selectResult = expectedResult
selectErr := errors.New("SELECT ERROR")
this.inner.selectError = selectErr

result, err := this.adapter.Select(context.Background(), "query", 1, 2, 3)

this.So(result, should.Equal, expectedResult)
this.So(err, should.Equal, selectErr)
this.So(this.inner.selectCalls, should.Equal, 1)
this.So(this.inner.selectStatement, should.Equal, "query")
this.So(this.inner.selectParameters, should.Equal, []any{1, 2, 3})
}
func (this *NormalizeContextCancellationConnectionPoolFixture) TestSelect_AdaptContextCancelled() {
expectedResult := new(FakeSelectResult)
this.inner.selectResult = expectedResult
this.inner.selectError = operationCanceledErr

result, err := this.adapter.Select(context.Background(), "query", 1, 2, 3)

this.So(result, should.Equal, expectedResult)
this.So(this.inner.selectCalls, should.Equal, 1)
this.So(this.inner.selectStatement, should.Equal, "query")
this.So(this.inner.selectParameters, should.Equal, []any{1, 2, 3})
this.So(errors.Is(err, operationCanceledErr), should.BeTrue)
this.So(errors.Is(err, context.Canceled), should.BeTrue)
}

func (this *NormalizeContextCancellationConnectionPoolFixture) TestContextCancellationErrorAdapter_NilError() {
err := this.adapter.normalizeContextCancellationError(nil)
this.So(err, should.BeNil)
}
func (this *NormalizeContextCancellationConnectionPoolFixture) TestContextCancellationErrorAdapter_GenericError() {
genericErr := errors.New("generic error")
err := this.adapter.normalizeContextCancellationError(genericErr)
this.So(err, should.Equal, genericErr)
}
func (this *NormalizeContextCancellationConnectionPoolFixture) TestContextCancellationErrorAdapter_OperationCanceledError() {
err := this.adapter.normalizeContextCancellationError(operationCanceledErr)
this.So(errors.Is(err, operationCanceledErr), should.BeTrue)
this.So(errors.Is(err, context.Canceled), should.BeTrue)
}
func (this *NormalizeContextCancellationConnectionPoolFixture) TestContextCancellationErrorAdapter_ClosedConnectionError() {
err := this.adapter.normalizeContextCancellationError(closedNetworkConnectionErr)
this.So(errors.Is(err, closedNetworkConnectionErr), should.BeTrue)
this.So(errors.Is(err, context.Canceled), should.BeTrue)
}

var (
operationCanceledErr = errors.New("operation was canceled")
closedNetworkConnectionErr = errors.New("use of closed network connection")
)
Loading