database: add lock support
This commit is contained in:
parent
6a9cf21fd4
commit
3a786ae020
@ -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()
|
||||||
}
|
}
|
||||||
|
@ -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
88
database/pgsql/lock.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
55
database/pgsql/lock_test.go
Normal file
55
database/pgsql/lock_test.go
Normal 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)
|
||||||
|
}
|
@ -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;
|
||||||
|
@ -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 {
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user