fix(11-08): bind nested callbacks to parent transaction

This commit is contained in:
Jakub Zych
2026-09-30 22:24:04 +02:00
parent 4d7823984e
commit 9526b6b638
2 changed files with 45 additions and 6 deletions

View File

@@ -19,8 +19,13 @@ type afterCommitKey struct{}
// afterCommitBuffer holds the callbacks registered inside one transaction.
type afterCommitBuffer struct {
mu sync.Mutex
fns []func(context.Context, *gorm.DB)
mu sync.Mutex
connPool gorm.ConnPool
fns []func(context.Context, *gorm.DB)
}
func (b *afterCommitBuffer) owns(db *gorm.DB) bool {
return b != nil && b.connPool != nil && db != nil && db.Statement != nil && db.Statement.ConnPool == b.connPool
}
func (b *afterCommitBuffer) add(fns ...func(context.Context, *gorm.DB)) {
@@ -55,12 +60,13 @@ func Transaction(ctx context.Context, gdb *gorm.DB, fn func(ctx context.Context,
ctx = context.Background()
}
if parent, ok := ctx.Value(afterCommitKey{}).(*afterCommitBuffer); ok && parent != nil {
if !transactionalHandle(gdb) {
if !parent.owns(gdb) {
return fmt.Errorf("lagoon: nested transaction requires the parent transaction handle")
}
child := &afterCommitBuffer{}
childCtx := context.WithValue(ctx, afterCommitKey{}, child)
err := gdb.WithContext(childCtx).Transaction(func(tx *gorm.DB) error {
child.connPool = tx.Statement.ConnPool
return fn(childCtx, tx)
})
if err != nil {
@@ -72,6 +78,7 @@ func Transaction(ctx context.Context, gdb *gorm.DB, fn func(ctx context.Context,
buf := &afterCommitBuffer{}
txCtx := context.WithValue(ctx, afterCommitKey{}, buf)
if err := gdb.WithContext(txCtx).Transaction(func(tx *gorm.DB) error {
buf.connPool = tx.Statement.ConnPool
return fn(txCtx, tx)
}); err != nil {
return err
@@ -90,9 +97,8 @@ func Transaction(ctx context.Context, gdb *gorm.DB, fn func(ctx context.Context,
// Anywhere else, fn runs immediately on db's connection.
//
// The handle fn receives always has an empty statement on the connection
// the work belongs to (the pool after a commit, the open transaction
// inside a plain gorm Transaction), whatever handle AfterCommit was called
// with. Queries through it never continue from the written model's
// the work belongs to, whatever handle AfterCommit was called with. Queries
// through it never continue from the written model's
// statement, even when they start with WithContext.
func AfterCommit(ctx context.Context, db *gorm.DB, fn func(ctx context.Context, db *gorm.DB)) {
if fn == nil {