-
Notifications
You must be signed in to change notification settings - Fork 9
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
bugfix: statement prepared in TX is closed with TX #117
Changes from all commits
f69c07b
c98533e
57c59cc
49ac151
13beb4b
1ca8bb4
622fc8b
8add63b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -167,7 +167,7 @@ func (db *DB) Query(ctx context.Context, s *Statement, inputArgs ...any) *Query | |
ctx = context.Background() | ||
} | ||
|
||
sqlstmt, err := db.prepareStmt(ctx, db.sqldb, s) | ||
sqlstmt, err := db.prepareStmt(ctx, s) | ||
if err != nil { | ||
return &Query{ctx: ctx, err: err} | ||
} | ||
|
@@ -180,25 +180,17 @@ func (db *DB) Query(ctx context.Context, s *Statement, inputArgs ...any) *Query | |
return &Query{sqlstmt: sqlstmt, stmt: s, db: db, pq: pq, ctx: ctx, err: nil} | ||
} | ||
|
||
// prepareSubstrate is an object that queries can be prepared on, e.g. a sql.DB | ||
// or sql.Conn. It is used in prepareStmt. | ||
type prepareSubstrate interface { | ||
PrepareContext(context.Context, string) (*sql.Stmt, error) | ||
} | ||
|
||
// prepareStmt prepares a Statement on a prepareSubstrate. It first checks in | ||
// the cache to see if it has already been prepared on the DB. | ||
// The prepareSubstrate must be assosiated with the same DB that prepareStmt is | ||
// a method of. | ||
func (db *DB) prepareStmt(ctx context.Context, ps prepareSubstrate, s *Statement) (*sql.Stmt, error) { | ||
// prepareStmt prepares a Statement on a DB. It first checks in the cache to | ||
// see if it has already been prepared. | ||
func (db *DB) prepareStmt(ctx context.Context, s *Statement) (*sql.Stmt, error) { | ||
var err error | ||
cacheMutex.RLock() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd like to see this tucked inside a cache implementation that uses the singleton pattern discussed prior, but not in this patch. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agree There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. PR #126 does this. |
||
// The statement ID is only removed from the cache when the finalizer is | ||
// run, so it is always in stmtDBCache. | ||
sqlstmt, ok := stmtDBCache[s.cacheID][db.cacheID] | ||
cacheMutex.RUnlock() | ||
if !ok { | ||
sqlstmt, err = ps.PrepareContext(ctx, s.te.SQL()) | ||
sqlstmt, err = db.sqldb.PrepareContext(ctx, s.te.SQL()) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
@@ -479,10 +471,9 @@ func (q *Query) GetAll(sliceArgs ...any) (err error) { | |
} | ||
|
||
type TX struct { | ||
sqltx *sql.Tx | ||
sqlconn *sql.Conn | ||
db *DB | ||
done int32 | ||
sqltx *sql.Tx | ||
db *DB | ||
done int32 | ||
} | ||
|
||
func (tx *TX) isDone() bool { | ||
|
@@ -501,15 +492,11 @@ func (db *DB) Begin(ctx context.Context, opts *TXOptions) (*TX, error) { | |
if ctx == nil { | ||
ctx = context.Background() | ||
} | ||
sqlconn, err := db.sqldb.Conn(ctx) | ||
if err != nil { | ||
return nil, err | ||
} | ||
sqltx, err := sqlconn.BeginTx(ctx, opts.plainTXOptions()) | ||
sqltx, err := db.sqldb.BeginTx(ctx, opts.plainTXOptions()) | ||
if err != nil { | ||
return nil, err | ||
} | ||
return &TX{sqltx: sqltx, sqlconn: sqlconn, db: db}, nil | ||
return &TX{sqltx: sqltx, db: db}, nil | ||
} | ||
|
||
// Commit commits the transaction. | ||
|
@@ -518,9 +505,6 @@ func (tx *TX) Commit() error { | |
if err == nil { | ||
err = tx.sqltx.Commit() | ||
} | ||
if cerr := tx.sqlconn.Close(); err == nil { | ||
err = cerr | ||
} | ||
return err | ||
} | ||
|
||
|
@@ -530,9 +514,6 @@ func (tx *TX) Rollback() error { | |
if err == nil { | ||
err = tx.sqltx.Rollback() | ||
} | ||
if cerr := tx.sqlconn.Close(); err == nil { | ||
err = cerr | ||
} | ||
return err | ||
} | ||
|
||
|
@@ -561,7 +542,7 @@ func (tx *TX) Query(ctx context.Context, s *Statement, inputArgs ...any) *Query | |
return &Query{ctx: ctx, err: ErrTXDone} | ||
} | ||
|
||
sqlstmt, err := tx.db.prepareStmt(ctx, tx.sqlconn, s) | ||
sqlstmt, err := tx.db.prepareStmt(ctx, s) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What about this?
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That could work but only for that specific case. If we had Another solution would be to check whether there is a max number of connections and issue a warning or even return an error. We could even require an environment variable to be set just to be explicit about the number of connections being a problem. The issue, once again, is the concurrency aspect. |
||
if err != nil { | ||
return &Query{ctx: ctx, err: 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.
I think we should have some protection against this still. I have a few ideas but I'm not sure about any of them.
NewDB
we return an error if the max connections is 1. The problem with this is that a deadlock can still if the number of in progress transactions matches the number connections in the pool. Also the user can still increase the max connections.SetMaxOpenConns
. Then we could not prepare on the DB if we know it would block. Again, the problem with this is that the user can still use the "plain" DB and we would not control it. A solution for this would be to also override the Open method and removePlainDB
however this would be a breaking change.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.
Regarding 2. I don't think that is the responsibility of SQLair, it should be done at the sql std library instead. Essentially we would be having a parallel "pool" implementation/accounting.