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 }