Implemented periodic ping to keep connection of transaction alive

This commit is contained in:
Lukas Jorg 2023-05-25 07:45:50 +02:00
parent c7f668094a
commit b42b67ff0b

160
main.go
View File

@ -32,15 +32,17 @@ type DB struct {
// Open initialize a new db connection, need to import driver first, e.g: // Open initialize a new db connection, need to import driver first, e.g:
// //
// import _ "github.com/go-sql-driver/mysql" // import _ "github.com/go-sql-driver/mysql"
// func main() { // func main() {
// db, err := gorm.Open("mysql", "user:password@/dbname?charset=utf8&parseTime=True&loc=Local") // db, err := gorm.Open("mysql", "user:password@/dbname?charset=utf8&parseTime=True&loc=Local")
// } // }
//
// GORM has wrapped some drivers, for easier to remember driver's import path, so you could import the mysql driver with // GORM has wrapped some drivers, for easier to remember driver's import path, so you could import the mysql driver with
// import _ "github.com/jinzhu/gorm/dialects/mysql" //
// // import _ "github.com/jinzhu/gorm/dialects/postgres" // import _ "github.com/jinzhu/gorm/dialects/mysql"
// // import _ "github.com/jinzhu/gorm/dialects/sqlite" // // import _ "github.com/jinzhu/gorm/dialects/postgres"
// // import _ "github.com/jinzhu/gorm/dialects/mssql" // // import _ "github.com/jinzhu/gorm/dialects/sqlite"
// // import _ "github.com/jinzhu/gorm/dialects/mssql"
func Open(dialect string, args ...interface{}) (db *DB, err error) { func Open(dialect string, args ...interface{}) (db *DB, err error) {
if len(args) == 0 { if len(args) == 0 {
err = errors.New("invalid database source") err = errors.New("invalid database source")
@ -121,7 +123,9 @@ func (s *DB) Dialect() Dialect {
} }
// Callback return `Callbacks` container, you could add/change/delete callbacks with it // Callback return `Callbacks` container, you could add/change/delete callbacks with it
// db.Callback().Create().Register("update_created_at", updateCreated) //
// db.Callback().Create().Register("update_created_at", updateCreated)
//
// Refer https://jinzhu.github.io/gorm/development.html#callbacks // Refer https://jinzhu.github.io/gorm/development.html#callbacks
func (s *DB) Callback() *Callback { func (s *DB) Callback() *Callback {
s.parent.callbacks = s.parent.callbacks.clone() s.parent.callbacks = s.parent.callbacks.clone()
@ -224,9 +228,10 @@ func (s *DB) Offset(offset interface{}) *DB {
} }
// Order specify order when retrieve records from database, set reorder to `true` to overwrite defined conditions // Order specify order when retrieve records from database, set reorder to `true` to overwrite defined conditions
// db.Order("name DESC") //
// db.Order("name DESC", true) // reorder // db.Order("name DESC")
// db.Order(gorm.Expr("name = ? DESC", "first")) // sql expression // db.Order("name DESC", true) // reorder
// db.Order(gorm.Expr("name = ? DESC", "first")) // sql expression
func (s *DB) Order(value interface{}, reorder ...bool) *DB { func (s *DB) Order(value interface{}, reorder ...bool) *DB {
return s.clone().search.Order(value, reorder...).db return s.clone().search.Order(value, reorder...).db
} }
@ -253,23 +258,26 @@ func (s *DB) Having(query interface{}, values ...interface{}) *DB {
} }
// Joins specify Joins conditions // Joins specify Joins conditions
// db.Joins("JOIN emails ON emails.user_id = users.id AND emails.email = ?", "jinzhu@example.org").Find(&user) //
// db.Joins("JOIN emails ON emails.user_id = users.id AND emails.email = ?", "jinzhu@example.org").Find(&user)
func (s *DB) Joins(query interface{}, args ...interface{}) *DB { func (s *DB) Joins(query interface{}, args ...interface{}) *DB {
return s.clone().search.Joins(query, args...).db return s.clone().search.Joins(query, args...).db
} }
// Scopes pass current database connection to arguments `func(*DB) *DB`, which could be used to add conditions dynamically // Scopes pass current database connection to arguments `func(*DB) *DB`, which could be used to add conditions dynamically
// func AmountGreaterThan1000(db *gorm.DB) *gorm.DB {
// return db.Where("amount > ?", 1000)
// }
// //
// func OrderStatus(status []string) func (db *gorm.DB) *gorm.DB { // func AmountGreaterThan1000(db *gorm.DB) *gorm.DB {
// return func (db *gorm.DB) *gorm.DB { // return db.Where("amount > ?", 1000)
// return db.Scopes(AmountGreaterThan1000).Where("status in (?)", status) // }
// } //
// } // func OrderStatus(status []string) func (db *gorm.DB) *gorm.DB {
// return func (db *gorm.DB) *gorm.DB {
// return db.Scopes(AmountGreaterThan1000).Where("status in (?)", status)
// }
// }
//
// db.Scopes(AmountGreaterThan1000, OrderStatus([]string{"paid", "shipped"})).Find(&orders)
// //
// db.Scopes(AmountGreaterThan1000, OrderStatus([]string{"paid", "shipped"})).Find(&orders)
// Refer https://jinzhu.github.io/gorm/crud.html#scopes // Refer https://jinzhu.github.io/gorm/crud.html#scopes
func (s *DB) Scopes(funcs ...func(*DB) *DB) *DB { func (s *DB) Scopes(funcs ...func(*DB) *DB) *DB {
for _, f := range funcs { for _, f := range funcs {
@ -356,8 +364,9 @@ func (s *DB) ScanRows(rows *sql.Rows, result interface{}) error {
} }
// Pluck used to query single column from a model as a map // Pluck used to query single column from a model as a map
// var ages []int64 //
// db.Find(&users).Pluck("age", &ages) // var ages []int64
// db.Find(&users).Pluck("age", &ages)
func (s *DB) Pluck(column string, value interface{}) *DB { func (s *DB) Pluck(column string, value interface{}) *DB {
return s.NewScope(s.Value).pluck(column, value).db return s.NewScope(s.Value).pluck(column, value).db
} }
@ -454,7 +463,8 @@ func (s *DB) Delete(value interface{}, where ...interface{}) *DB {
} }
// Raw use raw sql as conditions, won't run it unless invoked by other methods // Raw use raw sql as conditions, won't run it unless invoked by other methods
// db.Raw("SELECT name, age FROM users WHERE name = ?", 3).Scan(&result) //
// db.Raw("SELECT name, age FROM users WHERE name = ?", 3).Scan(&result)
func (s *DB) Raw(sql string, values ...interface{}) *DB { func (s *DB) Raw(sql string, values ...interface{}) *DB {
return s.clone().search.Raw(true).Where(sql, values...).db return s.clone().search.Raw(true).Where(sql, values...).db
} }
@ -469,10 +479,11 @@ func (s *DB) Exec(sql string, values ...interface{}) *DB {
} }
// Model specify the model you would like to run db operations // Model specify the model you would like to run db operations
// // update all users's name to `hello` //
// db.Model(&User{}).Update("name", "hello") // // update all users's name to `hello`
// // if user's primary key is non-blank, will use it as condition, then will only update the user's name to `hello` // db.Model(&User{}).Update("name", "hello")
// db.Model(&user).Update("name", "hello") // // if user's primary key is non-blank, will use it as condition, then will only update the user's name to `hello`
// db.Model(&user).Update("name", "hello")
func (s *DB) Model(value interface{}) *DB { func (s *DB) Model(value interface{}) *DB {
c := s.clone() c := s.clone()
c.Value = value c.Value = value
@ -530,32 +541,70 @@ func (s *DB) WrapInTx(f func(tx *DB) error) (err error) {
if _, ok := s.db.(*sql.Tx); ok { if _, ok := s.db.(*sql.Tx); ok {
// Already in a transaction // Already in a transaction
return f(s) return f(s)
} else { }
// Lets start a new transaction
tx := s.Begin() // Lets start a new transaction
if err = tx.Error; err != nil { tx := s.Begin()
return if err = tx.Error; err != nil {
} return err
panicked := true }
defer func() {
if panicked || err != nil { // Create a channel to stop the ping goroutine.
rollbackErr := tx.Rollback().Error stopTxPing := make(chan bool)
if rollbackErr != nil { // Start a goroutine that pings the database connection for a keep-alive.
if err == nil { go func() {
err = rollbackErr for {
} else { select {
err = fmt.Errorf("Transacton code and rollback failed: %s; %s", err, rollbackErr) // Stop the goroutine when the stop channel receives a value ..
} case <-stopTxPing:
return
// .. otherwise ping the database connection every 10 seconds.
case <-time.After(10 * time.Second):
err := tx.DB().Ping()
if err != nil {
tx.AddError(
fmt.Errorf(
"Could not ping database connection for transaction: %w",
err,
),
)
return
} }
} }
}()
err = f(tx)
if err == nil {
err = tx.Commit().Error
} }
panicked = false }()
return
panicked := true
defer func() {
if panicked || err != nil {
rollbackErr := tx.Rollback().Error
if rollbackErr != nil {
if err == nil {
err = rollbackErr
} else {
err = fmt.Errorf("Transacton code and rollback failed: %s; %s", err, rollbackErr)
}
}
}
}()
err = f(tx)
// As soon as the inner stack has returned, stop the ping goroutine. As the transaction will be
// only committed after this point, the ping would fail and the goroutine will exit.
stopTxPing <- true
// Last but not least, close the stop ping channel.
close(stopTxPing)
if err == nil {
err = tx.Commit().Error
} }
panicked = false
return err
} }
// SkipAssocSave disables saving of associations // SkipAssocSave disables saving of associations
@ -674,7 +723,8 @@ func (s *DB) RemoveIndex(indexName string) *DB {
} }
// AddForeignKey Add foreign key to the given scope, e.g: // AddForeignKey Add foreign key to the given scope, e.g:
// db.Model(&User{}).AddForeignKey("city_id", "cities(id)", "RESTRICT", "RESTRICT") //
// db.Model(&User{}).AddForeignKey("city_id", "cities(id)", "RESTRICT", "RESTRICT")
func (s *DB) AddForeignKey(field string, dest string, onDelete string, onUpdate string) *DB { func (s *DB) AddForeignKey(field string, dest string, onDelete string, onUpdate string) *DB {
scope := s.NewScope(s.Value) scope := s.NewScope(s.Value)
scope.addForeignKey(field, dest, onDelete, onUpdate) scope.addForeignKey(field, dest, onDelete, onUpdate)
@ -682,7 +732,8 @@ func (s *DB) AddForeignKey(field string, dest string, onDelete string, onUpdate
} }
// RemoveForeignKey Remove foreign key from the given scope, e.g: // RemoveForeignKey Remove foreign key from the given scope, e.g:
// db.Model(&User{}).RemoveForeignKey("city_id", "cities(id)") //
// db.Model(&User{}).RemoveForeignKey("city_id", "cities(id)")
func (s *DB) RemoveForeignKey(field string, dest string) *DB { func (s *DB) RemoveForeignKey(field string, dest string) *DB {
scope := s.clone().NewScope(s.Value) scope := s.clone().NewScope(s.Value)
scope.removeForeignKey(field, dest) scope.removeForeignKey(field, dest)
@ -712,7 +763,8 @@ func (s *DB) Association(column string) *Association {
} }
// Preload preload associations with given conditions // Preload preload associations with given conditions
// db.Preload("Orders", "state NOT IN (?)", "cancelled").Find(&users) //
// db.Preload("Orders", "state NOT IN (?)", "cancelled").Find(&users)
func (s *DB) Preload(column string, conditions ...interface{}) *DB { func (s *DB) Preload(column string, conditions ...interface{}) *DB {
return s.clone().search.Preload(column, conditions...).db return s.clone().search.Preload(column, conditions...).db
} }