You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
89 lines
2.2 KiB
89 lines
2.2 KiB
package db
|
|
|
|
import (
|
|
"database/sql"
|
|
"fmt"
|
|
"ims/util/backoff"
|
|
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// SQL statements for MySQL advisory locks.
|
|
// https://dev.mysql.com/doc/refman/8.4/en/locking-functions.html
|
|
const (
|
|
// GET_LOCK(str, timeout) → int (1: lock acquired, 0: lock not acquired, NULL: an error occurred).
|
|
mysqlGetLock = "SELECT GET_LOCK('%s', 0)"
|
|
|
|
// RELEASE_LOCK(str) → int (1: lock released, 0: lock not released, NULL: lock does not exist).
|
|
mysqlReleaseLock = "SELECT RELEASE_LOCK('%s')"
|
|
)
|
|
|
|
// mysqlLock represents a MySQL advisory mysqlLock.
|
|
type mysqlLock struct {
|
|
tx *gorm.DB
|
|
lockKey string
|
|
}
|
|
|
|
func (a *mysqlLock) execute(sqlstr string) (bool, error) {
|
|
var result sql.NullInt64
|
|
if err := a.tx.Raw(sqlstr).Scan(&result).Error; err == nil {
|
|
if !result.Valid {
|
|
return false, ErrAcquireLock
|
|
}
|
|
switch result.Int64 {
|
|
case 1:
|
|
return true, nil
|
|
case 0:
|
|
return false, ErrAcquireLock
|
|
}
|
|
}
|
|
return false, fmt.Errorf("%w: %s", ErrExecSQL, sqlstr)
|
|
}
|
|
|
|
func (a *mysqlLock) acquire() (func() error, error) {
|
|
sqlstr := fmt.Sprintf(mysqlGetLock, a.lockKey)
|
|
if ok, err := a.execute(sqlstr); err != nil || !ok {
|
|
return nil, fmt.Errorf("%w for key %s: %v", ErrAcquireLock, a.lockKey, err)
|
|
}
|
|
return a.release, nil
|
|
}
|
|
|
|
func (a *mysqlLock) release() error {
|
|
sqlstr := fmt.Sprintf(mysqlReleaseLock, a.lockKey)
|
|
if success, err := a.execute(sqlstr); err != nil || !success {
|
|
return fmt.Errorf("%w for key %s: %v", ErrReleaseLock, a.lockKey, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// acquire acquires a MySQL advisory lock.
|
|
func acquire(tx *gorm.DB, lockKey string) (func() error, error) {
|
|
lock := &mysqlLock{tx: tx, lockKey: lockKey}
|
|
return lock.acquire()
|
|
}
|
|
|
|
// Acquire acquires a MySQL advisory lock.
|
|
// Returns a release function and an error.
|
|
//
|
|
// It's the responsibility of the caller to release the lock by calling the release function.
|
|
func AcquireLock(tx *gorm.DB, lockKey string, options *backoff.Options) (release func() error, err error) {
|
|
if options == nil {
|
|
release, err = acquire(tx, lockKey)
|
|
return
|
|
}
|
|
|
|
acquireFunc := func() error {
|
|
releaseFn, acquireErr := acquire(tx, lockKey)
|
|
if acquireErr != nil {
|
|
return acquireErr
|
|
}
|
|
release = releaseFn
|
|
return nil
|
|
}
|
|
|
|
err = backoff.Retry(acquireFunc, func(o *backoff.Options) {
|
|
*o = *options
|
|
})
|
|
|
|
return release, err
|
|
}
|
|
|