database: add lock support

This commit is contained in:
Quentin Machu 2016-01-08 14:42:07 -05:00 committed by Jimmy Zelinskie
parent 6a9cf21fd4
commit 3a786ae020
7 changed files with 179 additions and 7 deletions

View File

@ -1,6 +1,9 @@
package database package database
import "errors" import (
"errors"
"time"
)
var ( var (
// ErrTransaction is an error that occurs when a database transaction fails. // ErrTransaction is an error that occurs when a database transaction fails.
@ -39,9 +42,9 @@ type Datastore interface {
GetKeyValue(key string) (string, error) GetKeyValue(key string) (string, error)
// Lock // Lock
// Lock(name string, duration time.Duration, owner string) (bool, time.Time) Lock(name string, owner string, duration time.Duration, renew bool) (bool, time.Time)
// Unlock(name, owner string) Unlock(name, owner string)
// LockInfo(name string) (string, time.Time, error) FindLock(name string) (string, time.Time, error)
Close() Close()
} }

View File

@ -352,6 +352,6 @@ func createNV(features []database.FeatureVersion) (map[string]*database.FeatureV
} }
func (pgSQL *pgSQL) DeleteLayer(name string) error { func (pgSQL *pgSQL) DeleteLayer(name string) error {
// TODO // TODO(Quentin-M): Implement and test me.
return nil return nil
} }

88
database/pgsql/lock.go Normal file
View File

@ -0,0 +1,88 @@
package pgsql
import (
"database/sql"
"time"
cerrors "github.com/coreos/clair/utils/errors"
)
// Lock tries to set a temporary lock in the database.
//
// Lock does not block, instead, it returns true and its expiration time
// is the lock has been successfully acquired or false otherwise
func (pgSQL *pgSQL) Lock(name string, owner string, duration time.Duration, renew bool) (bool, time.Time) {
if name == "" || owner == "" || duration == 0 {
log.Warning("could not create an invalid lock")
return false, time.Time{}
}
// Prune locks.
pgSQL.pruneLocks()
// Compute expiration.
until := time.Now().Add(duration)
if renew {
// Renew lock.
r, err := pgSQL.Exec(getQuery("u_lock"), name, owner, until)
if err != nil {
handleError("u_lock", err)
return false, until
}
if n, _ := r.RowsAffected(); n > 0 {
// Updated successfully.
return true, until
}
}
// Lock.
_, err := pgSQL.Exec(getQuery("i_lock"), name, owner, until)
if err != nil {
if !isErrUniqueViolation(err) {
handleError("i_lock", err)
}
return false, until
}
return true, until
}
// Unlock unlocks a lock specified by its name if I own it
func (pgSQL *pgSQL) Unlock(name, owner string) {
if name == "" || owner == "" {
log.Warning("could not delete an invalid lock")
return
}
pgSQL.Exec(getQuery("r_lock"), name, owner)
}
// FindLock returns the owner of a lock specified by its name and its
// expiration time.
func (pgSQL *pgSQL) FindLock(name string) (string, time.Time, error) {
if name == "" {
log.Warning("could not find an invalid lock")
return "", time.Time{}, cerrors.NewBadRequestError("could not find an invalid lock")
}
var owner string
var until time.Time
err := pgSQL.QueryRow(getQuery("f_lock"), name).Scan(&owner, &until)
if err == sql.ErrNoRows {
return owner, until, cerrors.ErrNotFound
}
if err != nil {
return owner, until, handleError("f_lock", err)
}
return owner, until, nil
}
// pruneLocks removes every expired locks from the database
func (pgSQL *pgSQL) pruneLocks() {
if _, err := pgSQL.Exec(getQuery("r_lock_expired")); err != nil {
handleError("r_lock_expired", err)
}
}

View File

@ -0,0 +1,55 @@
package pgsql
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestLock(t *testing.T) {
datastore, err := OpenForTest("InsertNamespace", false)
if err != nil {
t.Error(err)
return
}
defer datastore.Close()
var l bool
var et time.Time
// Create a first lock.
l, _ = datastore.Lock("test1", "owner1", time.Minute, false)
assert.True(t, l)
// Try to lock the same lock with another owner.
l, _ = datastore.Lock("test1", "owner2", time.Minute, true)
assert.False(t, l)
l, _ = datastore.Lock("test1", "owner2", time.Minute, false)
assert.False(t, l)
// Renew the lock.
l, _ = datastore.Lock("test1", "owner1", 2*time.Minute, true)
assert.True(t, l)
// Unlock and then relock by someone else.
datastore.Unlock("test1", "owner1")
l, et = datastore.Lock("test1", "owner2", time.Minute, false)
assert.True(t, l)
// LockInfo
o, et2, err := datastore.FindLock("test1")
assert.Nil(t, err)
assert.Equal(t, "owner2", o)
assert.Equal(t, et.Second(), et2.Second())
// Create a second lock which is actually already expired ...
l, _ = datastore.Lock("test2", "owner1", -time.Minute, false)
assert.True(t, l)
// Take over the lock
l, _ = datastore.Lock("test2", "owner2", time.Minute, false)
assert.True(t, l)
}

View File

@ -15,12 +15,13 @@ CREATE TABLE IF NOT EXISTS Layer (
id SERIAL PRIMARY KEY, id SERIAL PRIMARY KEY,
name VARCHAR(128) NOT NULL UNIQUE, name VARCHAR(128) NOT NULL UNIQUE,
engineversion SMALLINT NOT NULL, engineversion SMALLINT NOT NULL,
parent_id INT NULL REFERENCES Layer, parent_id INT NULL REFERENCES Layer ON DELETE CASCADE,
namespace_id INT NULL REFERENCES Namespace); namespace_id INT NULL REFERENCES Namespace);
CREATE INDEX ON Layer (parent_id); CREATE INDEX ON Layer (parent_id);
CREATE INDEX ON Layer (namespace_id); CREATE INDEX ON Layer (namespace_id);
-- ----------------------------------------------------- -- -----------------------------------------------------
-- Table Feature -- Table Feature
-- ----------------------------------------------------- -- -----------------------------------------------------
@ -31,6 +32,7 @@ CREATE TABLE IF NOT EXISTS Feature (
UNIQUE (namespace_id, name)); UNIQUE (namespace_id, name));
-- ----------------------------------------------------- -- -----------------------------------------------------
-- Table FeatureVersion -- Table FeatureVersion
-- ----------------------------------------------------- -- -----------------------------------------------------
@ -113,6 +115,18 @@ CREATE TABLE IF NOT EXISTS KeyValue (
key VARCHAR(128) NOT NULL UNIQUE, key VARCHAR(128) NOT NULL UNIQUE,
value TEXT); value TEXT);
-- -----------------------------------------------------
-- Table Lock
-- -----------------------------------------------------
CREATE TABLE IF NOT EXISTS Lock (
id SERIAL PRIMARY KEY,
name VARCHAR(64) NOT NULL UNIQUE,
owner VARCHAR(64) NOT NULL,
until TIMESTAMP WITH TIME ZONE);
CREATE INDEX ON Lock (owner);
-- +goose Down -- +goose Down
DROP TABLE IF EXISTS Namespace, DROP TABLE IF EXISTS Namespace,
@ -124,4 +138,5 @@ DROP TABLE IF EXISTS Namespace,
Vulnerability_FixedIn_Feature, Vulnerability_FixedIn_Feature,
Vulnerability_Affects_FeatureVersion, Vulnerability_Affects_FeatureVersion,
KeyValue KeyValue
Lock
CASCADE; CASCADE;

View File

@ -131,6 +131,17 @@ func init() {
SELECT $1, fv.id, $2 SELECT $1, fv.id, $2
FROM FeatureVersion fv FROM FeatureVersion fv
WHERE fv.id = ANY($3::integer[])` WHERE fv.id = ANY($3::integer[])`
// lock.go
queries["i_lock"] = `INSERT INTO Lock(name, owner, until) VALUES($1, $2, $3)`
queries["f_lock"] = `SELECT owner, until FROM Lock WHERE name = $1`
queries["u_lock"] = `UPDATE Lock SET until = $3 WHERE name = $1 AND owner = $2`
queries["r_lock"] = `DELETE FROM Lock WHERE name = $1 AND owner = $2`
queries["r_lock_expired"] = `DELETE FROM LOCK WHERE until < CURRENT_TIMESTAMP`
} }
func getQuery(name string) string { func getQuery(name string) string {

View File

@ -101,7 +101,7 @@ func Run(config *config.UpdaterConfig, st *utils.Stopper) {
} }
continue continue
} else { } else {
lockOwner, lockExpiration, err := database.LockInfo(flagName) lockOwner, lockExpiration, err := database.FindLock(flagName)
if err != nil { if err != nil {
log.Debug("update lock is already taken") log.Debug("update lock is already taken")
nextUpdate = hasLockUntil nextUpdate = hasLockUntil