b0142e1982
pruneLocks could create deadlocked transactions on PostgreSQL if multiple locks expired and pruneLocks is called by multiple instances. Also adds some logging.
157 lines
5.3 KiB
Go
157 lines
5.3 KiB
Go
// Copyright 2015 clair authors
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package database
|
|
|
|
import (
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/barakmich/glog"
|
|
cerrors "github.com/coreos/clair/utils/errors"
|
|
"github.com/google/cayley"
|
|
"github.com/google/cayley/graph"
|
|
"github.com/google/cayley/graph/path"
|
|
)
|
|
|
|
// Lock tries to set a temporary lock in the database.
|
|
// If a lock already exists with the given name/owner, then the lock is renewed
|
|
//
|
|
// Lock does not block, instead, it returns true and its expiration time
|
|
// is the lock has been successfully acquired or false otherwise
|
|
func Lock(name string, duration time.Duration, owner string) (bool, time.Time) {
|
|
pruneLocks()
|
|
|
|
until := time.Now().Add(duration)
|
|
untilString := strconv.FormatInt(until.Unix(), 10)
|
|
|
|
// Try to get the expiration time of a lock with the same name/owner
|
|
currentExpiration, err := toValue(cayley.StartPath(store, name).Has("locked_by", owner).Out("locked_until"))
|
|
if err == nil && currentExpiration != "" {
|
|
// Renew our lock
|
|
if currentExpiration == untilString {
|
|
return true, until
|
|
}
|
|
|
|
t := cayley.NewTransaction()
|
|
t.RemoveQuad(cayley.Quad(name, "locked_until", currentExpiration, ""))
|
|
t.AddQuad(cayley.Quad(name, "locked_until", untilString, ""))
|
|
// It is not necessary to verify if the lock is ours again in the transaction
|
|
// because if someone took it, the lock's current expiration probably changed and the transaction will fail
|
|
return store.ApplyTransaction(t) == nil, until
|
|
}
|
|
|
|
t := cayley.NewTransaction()
|
|
t.AddQuad(cayley.Quad(name, "locked", "locked", "")) // Necessary to make the transaction fails if the lock already exists (and has not been pruned)
|
|
t.AddQuad(cayley.Quad(name, "locked_until", untilString, ""))
|
|
t.AddQuad(cayley.Quad(name, "locked_by", owner, ""))
|
|
|
|
glog.SetStderrThreshold("FATAL")
|
|
success := store.ApplyTransaction(t) == nil
|
|
glog.SetStderrThreshold("ERROR")
|
|
|
|
return success, until
|
|
}
|
|
|
|
// Unlock unlocks a lock specified by its name if I own it
|
|
func Unlock(name, owner string) {
|
|
pruneLocks()
|
|
|
|
unlocked := 0
|
|
it, _ := cayley.StartPath(store, name).Has("locked", "locked").Has("locked_by", owner).Save("locked_until", "locked_until").BuildIterator().Optimize()
|
|
defer it.Close()
|
|
for cayley.RawNext(it) {
|
|
tags := make(map[string]graph.Value)
|
|
it.TagResults(tags)
|
|
|
|
t := cayley.NewTransaction()
|
|
t.RemoveQuad(cayley.Quad(name, "locked", "locked", ""))
|
|
t.RemoveQuad(cayley.Quad(name, "locked_until", store.NameOf(tags["locked_until"]), ""))
|
|
t.RemoveQuad(cayley.Quad(name, "locked_by", owner, ""))
|
|
err := store.ApplyTransaction(t)
|
|
if err != nil {
|
|
log.Errorf("failed transaction (Unlock): %s", err)
|
|
}
|
|
|
|
unlocked++
|
|
}
|
|
if it.Err() != nil {
|
|
log.Errorf("failed query in Unlock: %s", it.Err())
|
|
}
|
|
if unlocked > 1 {
|
|
// We should never see this, it would mean that our database doesn't ensure quad uniqueness
|
|
// and that the entire lock system is jeopardized.
|
|
log.Errorf("found inconsistency in Unlock: matched %d times a locked named: %s", unlocked, name)
|
|
}
|
|
}
|
|
|
|
// LockInfo returns the owner of a lock specified by its name and its
|
|
// expiration time
|
|
func LockInfo(name string) (string, time.Time, error) {
|
|
it, _ := cayley.StartPath(store, name).Has("locked", "locked").Save("locked_until", "locked_until").Save("locked_by", "locked_by").BuildIterator().Optimize()
|
|
defer it.Close()
|
|
for cayley.RawNext(it) {
|
|
tags := make(map[string]graph.Value)
|
|
it.TagResults(tags)
|
|
|
|
tt, _ := strconv.ParseInt(store.NameOf(tags["locked_until"]), 10, 64)
|
|
return store.NameOf(tags["locked_by"]), time.Unix(tt, 0), nil
|
|
}
|
|
if it.Err() != nil {
|
|
log.Errorf("failed query in LockInfo: %s", it.Err())
|
|
return "", time.Time{}, ErrBackendException
|
|
}
|
|
|
|
return "", time.Time{}, cerrors.ErrNotFound
|
|
}
|
|
|
|
// pruneLocks removes every expired locks from the database
|
|
func pruneLocks() {
|
|
now := time.Now()
|
|
|
|
// Delete every expired locks
|
|
it, _ := cayley.StartPath(store, "locked").In("locked").Save("locked_until", "locked_until").Save("locked_by", "locked_by").BuildIterator().Optimize()
|
|
defer it.Close()
|
|
for cayley.RawNext(it) {
|
|
tags := make(map[string]graph.Value)
|
|
it.TagResults(tags)
|
|
|
|
n := store.NameOf(it.Result())
|
|
t := store.NameOf(tags["locked_until"])
|
|
o := store.NameOf(tags["locked_by"])
|
|
tt, _ := strconv.ParseInt(t, 10, 64)
|
|
|
|
if now.Unix() > tt {
|
|
log.Debugf("Lock %s owned by %s has expired.", n, o)
|
|
|
|
tr := cayley.NewTransaction()
|
|
tr.RemoveQuad(cayley.Quad(n, "locked", "locked", ""))
|
|
tr.RemoveQuad(cayley.Quad(n, "locked_until", t, ""))
|
|
tr.RemoveQuad(cayley.Quad(n, "locked_by", o, ""))
|
|
err := store.ApplyTransaction(tr)
|
|
if err != nil {
|
|
log.Errorf("failed transaction (pruneLocks): %s", err)
|
|
}
|
|
}
|
|
}
|
|
if it.Err() != nil {
|
|
log.Errorf("failed query in Unlock: %s", it.Err())
|
|
}
|
|
}
|
|
|
|
// getLockedNodes returns every nodes that are currently locked
|
|
func getLockedNodes() *path.Path {
|
|
return cayley.StartPath(store, "locked").In("locked")
|
|
}
|