Skip to content

Commit

Permalink
database: add lock support
Browse files Browse the repository at this point in the history
  • Loading branch information
Quentin-M authored and jzelinskie committed Feb 24, 2016
1 parent 6a9cf21 commit 3a786ae
Show file tree
Hide file tree
Showing 7 changed files with 179 additions and 7 deletions.
11 changes: 7 additions & 4 deletions database/database.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package database

import "errors"
import (
"errors"
"time"
)

var (
// ErrTransaction is an error that occurs when a database transaction fails.
Expand Down Expand Up @@ -39,9 +42,9 @@ type Datastore interface {
GetKeyValue(key string) (string, error)

// Lock
// Lock(name string, duration time.Duration, owner string) (bool, time.Time)
// Unlock(name, owner string)
// LockInfo(name string) (string, time.Time, error)
Lock(name string, owner string, duration time.Duration, renew bool) (bool, time.Time)
Unlock(name, owner string)
FindLock(name string) (string, time.Time, error)

Close()
}
2 changes: 1 addition & 1 deletion database/pgsql/layer.go
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,6 @@ func createNV(features []database.FeatureVersion) (map[string]*database.FeatureV
}

func (pgSQL *pgSQL) DeleteLayer(name string) error {
// TODO
// TODO(Quentin-M): Implement and test me.
return nil
}
88 changes: 88 additions & 0 deletions database/pgsql/lock.go
Original file line number Diff line number Diff line change
@@ -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 changes: 55 additions & 0 deletions database/pgsql/lock_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
17 changes: 16 additions & 1 deletion database/pgsql/migrations/20151222113213_Initial.sql
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,13 @@ CREATE TABLE IF NOT EXISTS Layer (
id SERIAL PRIMARY KEY,
name VARCHAR(128) NOT NULL UNIQUE,
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);

CREATE INDEX ON Layer (parent_id);
CREATE INDEX ON Layer (namespace_id);


-- -----------------------------------------------------
-- Table Feature
-- -----------------------------------------------------
Expand All @@ -31,6 +32,7 @@ CREATE TABLE IF NOT EXISTS Feature (

UNIQUE (namespace_id, name));


-- -----------------------------------------------------
-- Table FeatureVersion
-- -----------------------------------------------------
Expand Down Expand Up @@ -113,6 +115,18 @@ CREATE TABLE IF NOT EXISTS KeyValue (
key VARCHAR(128) NOT NULL UNIQUE,
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

DROP TABLE IF EXISTS Namespace,
Expand All @@ -124,4 +138,5 @@ DROP TABLE IF EXISTS Namespace,
Vulnerability_FixedIn_Feature,
Vulnerability_Affects_FeatureVersion,
KeyValue
Lock
CASCADE;
11 changes: 11 additions & 0 deletions database/pgsql/queries.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,17 @@ func init() {
SELECT $1, fv.id, $2
FROM FeatureVersion fv
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 {
Expand Down
2 changes: 1 addition & 1 deletion updater/updater.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ func Run(config *config.UpdaterConfig, st *utils.Stopper) {
}
continue
} else {
lockOwner, lockExpiration, err := database.LockInfo(flagName)
lockOwner, lockExpiration, err := database.FindLock(flagName)
if err != nil {
log.Debug("update lock is already taken")
nextUpdate = hasLockUntil
Expand Down

0 comments on commit 3a786ae

Please sign in to comment.