concourse/atc/db/lock/lock.go

293 lines
5.4 KiB
Go

package lock
import (
"database/sql"
"errors"
"fmt"
"hash/crc32"
"strconv"
"strings"
"sync"
"code.cloudfoundry.org/lager"
)
const (
LockTypeResourceConfigChecking = iota
LockTypeBuildTracking
LockTypePipelineScheduling
LockTypeBatch
LockTypeVolumeCreating
LockTypeContainerCreating
LockTypeDatabaseMigration
)
var ErrLostLock = errors.New("lock was lost while held, possibly due to connection breakage")
func NewBuildTrackingLockID(buildID int) LockID {
return LockID{LockTypeBuildTracking, buildID}
}
func NewResourceConfigCheckingLockID(resourceConfigID int) LockID {
return LockID{LockTypeResourceConfigChecking, resourceConfigID}
}
func NewPipelineSchedulingLockLockID(pipelineID int) LockID {
return LockID{LockTypePipelineScheduling, pipelineID}
}
func NewTaskLockID(taskName string) LockID {
return LockID{LockTypeBatch, lockIDFromString(taskName)}
}
func NewVolumeCreatingLockID(volumeID int) LockID {
return LockID{LockTypeVolumeCreating, volumeID}
}
func NewContainerCreatingLockID() LockID {
return LockID{LockTypeContainerCreating}
}
func NewDatabaseMigrationLockID() LockID {
return LockID{LockTypeDatabaseMigration}
}
//go:generate counterfeiter . LockFactory
type LockFactory interface {
Acquire(logger lager.Logger, ids LockID) (Lock, bool, error)
}
type lockFactory struct {
db LockDB
locks lockRepo
acquireMutex *sync.Mutex
acquireFunc LogFunc
releaseFunc LogFunc
}
type LogFunc func(logger lager.Logger, id LockID)
func NewLockFactory(
conn *sql.DB,
acquire LogFunc,
release LogFunc,
) LockFactory {
return &lockFactory{
db: &lockDB{
conn: conn,
mutex: &sync.Mutex{},
},
acquireFunc: acquire,
releaseFunc: release,
locks: lockRepo{
locks: map[string]bool{},
mutex: &sync.Mutex{},
},
acquireMutex: &sync.Mutex{},
}
}
func NewTestLockFactory(db LockDB) LockFactory {
return &lockFactory{
db: db,
locks: lockRepo{
locks: map[string]bool{},
mutex: &sync.Mutex{},
},
acquireMutex: &sync.Mutex{},
acquireFunc: func(logger lager.Logger, id LockID) {},
releaseFunc: func(logger lager.Logger, id LockID) {},
}
}
func (f *lockFactory) Acquire(logger lager.Logger, id LockID) (Lock, bool, error) {
l := &lock{
logger: logger,
db: f.db,
id: id,
locks: f.locks,
acquireMutex: f.acquireMutex,
acquired: f.acquireFunc,
released: f.releaseFunc,
}
acquired, err := l.Acquire()
if err != nil {
return nil, false, err
}
if !acquired {
return nil, false, nil
}
return l, true, nil
}
//go:generate counterfeiter . Lock
type Lock interface {
Release() error
}
//go:generate counterfeiter . LockDB
type LockDB interface {
Acquire(id LockID) (bool, error)
Release(id LockID) (bool, error)
}
type lock struct {
id LockID
logger lager.Logger
db LockDB
locks lockRepo
acquireMutex *sync.Mutex
acquired LogFunc
released LogFunc
}
func (l *lock) Acquire() (bool, error) {
l.acquireMutex.Lock()
defer l.acquireMutex.Unlock()
logger := l.logger.Session("acquire", lager.Data{"id": l.id})
if l.locks.IsRegistered(l.id) {
logger.Debug("not-acquired-already-held-locally")
return false, nil
}
acquired, err := l.db.Acquire(l.id)
if err != nil {
logger.Error("failed-to-register-in-db", err)
return false, err
}
if !acquired {
logger.Debug("not-acquired-already-held-in-db")
return false, nil
}
l.locks.Register(l.id)
l.acquired(logger, l.id)
return true, nil
}
func (l *lock) Release() error {
logger := l.logger.Session("release", lager.Data{"id": l.id})
released, err := l.db.Release(l.id)
if err != nil {
logger.Error("failed-to-release-in-db-but-continuing-anyway", err)
}
l.locks.Unregister(l.id)
if !released {
logger.Error("failed-to-release", ErrLostLock)
return ErrLostLock
}
l.released(logger, l.id)
return nil
}
type lockDB struct {
conn *sql.DB
mutex *sync.Mutex
}
func (db *lockDB) Acquire(id LockID) (bool, error) {
db.mutex.Lock()
defer db.mutex.Unlock()
var acquired bool
err := db.conn.QueryRow(`SELECT pg_try_advisory_lock(`+id.toDBParams()+`)`, id.toDBArgs()...).Scan(&acquired)
if err != nil {
return false, err
}
return acquired, nil
}
func (db *lockDB) Release(id LockID) (bool, error) {
db.mutex.Lock()
defer db.mutex.Unlock()
var released bool
err := db.conn.QueryRow(`SELECT pg_advisory_unlock(`+id.toDBParams()+`)`, id.toDBArgs()...).Scan(&released)
if err != nil {
return false, err
}
return released, nil
}
type lockRepo struct {
locks map[string]bool
mutex *sync.Mutex
}
func (lr lockRepo) IsRegistered(id LockID) bool {
lr.mutex.Lock()
defer lr.mutex.Unlock()
if _, ok := lr.locks[id.toKey()]; ok {
return true
}
return false
}
func (lr lockRepo) Register(id LockID) {
lr.mutex.Lock()
defer lr.mutex.Unlock()
lr.locks[id.toKey()] = true
}
func (lr lockRepo) Unregister(id LockID) {
lr.mutex.Lock()
defer lr.mutex.Unlock()
delete(lr.locks, id.toKey())
}
type LockID []int
func (l LockID) toKey() string {
s := []string{}
for i := range l {
s = append(s, strconv.Itoa(l[i]))
}
return strings.Join(s, "+")
}
func (l LockID) toDBParams() string {
s := []string{}
for i := range l {
s = append(s, fmt.Sprintf("$%d", i+1))
}
return strings.Join(s, ",")
}
func (l LockID) toDBArgs() []interface{} {
result := []interface{}{}
for i := range l {
result = append(result, l[i])
}
return result
}
func lockIDFromString(taskName string) int {
return int(int32(crc32.ChecksumIEEE([]byte(taskName))))
}