Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
333aec5
Passes PSQL, SQLite and MSSQL
guillep2k Jan 20, 2020
70a2a8d
Move to upsert strategy; all tests work
guillep2k Jan 21, 2020
677e49f
Merge branch 'master' into bylock-indexes
guillep2k Jan 21, 2020
d834fb1
Use a LockedResource to numerate issues and prs
guillep2k Jan 21, 2020
58ac901
Fix tests and reserved keyword
guillep2k Jan 21, 2020
aa3797c
Fix unit tests
guillep2k Jan 21, 2020
95db48a
Fix export comments
guillep2k Jan 22, 2020
d8ad174
A little refactoring and better function naming
guillep2k Jan 22, 2020
6bec3d5
Merge branch 'master' into bylock-indexes
guillep2k Jan 22, 2020
e747a2c
Support LockType == "" and LockKey == 0
guillep2k Jan 22, 2020
afeb6f0
Merge branch 'master' into bylock-indexes
guillep2k Jan 22, 2020
c503f0d
Prepare for merge
guillep2k Jan 28, 2020
9b7ec1d
Merge branch 'master' into bylock-indexes
guillep2k Jan 28, 2020
1792664
Go simple
guillep2k Jan 28, 2020
ce6c24f
Improve test legibility
guillep2k Jan 30, 2020
15ffbb4
Fix typo
guillep2k Jan 30, 2020
ea9c875
Remove dead code
guillep2k Jan 30, 2020
9cb79c9
Merge branch 'master' into bylock-indexes
guillep2k Jan 30, 2020
d185a4f
Prepare for merge
guillep2k Feb 1, 2020
f46eaf5
Merge branch 'master' into bylock-indexes
guillep2k Feb 1, 2020
621c9d6
Prepare to merge
guillep2k Feb 12, 2020
17fa5e1
Merge branch 'master' into bylock-indexes
guillep2k Feb 12, 2020
b30094b
Merge branch 'master' into bylock-indexes
guillep2k Feb 15, 2020
299d313
Merge branch 'master' into bylock-indexes
guillep2k Feb 16, 2020
cea7c4f
Merge branch 'master' into bylock-indexes
guillep2k Feb 20, 2020
2311de3
Merge branch 'master' into bylock-indexes
guillep2k Feb 29, 2020
7e280a4
Merge branch 'master' into bylock-indexes
guillep2k May 2, 2020
15e407b
Code review suggestions by @lunny
guillep2k May 2, 2020
dd85873
Ignore SQLite3 integration when _txlock=immediate
guillep2k May 2, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Move to upsert strategy; all tests work
  • Loading branch information
guillep2k committed Jan 21, 2020
commit 70a2a8df2d6930b66c1da562296a129bf478e6e5
61 changes: 29 additions & 32 deletions integrations/locked_resource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,19 @@ import (
"time"

"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/log"

"github.com/stretchr/testify/assert"
)

const (
// Note: these values might require tuning
before = 500 * time.Millisecond
after = 1000 * time.Millisecond
tolerance = 200 * time.Millisecond
// The tests will fail if the waiter function takes less than
// blockerDelay minus tolerance to complete.
// Note: these values might require tuning in order to avoid
// false negatives.
waiterDelay = 100 * time.Millisecond
blockerDelay = 200 * time.Millisecond
tolerance = 50 * time.Millisecond // Should be <= (blockerDelay-waiterDelay)/2
)

type waitResult struct {
Expand All @@ -35,37 +39,31 @@ func TestLockedResource(t *testing.T) {
// the more certain we are the second goroutine is waiting.

// This check **must** fail as we're not blocking anything
assert.Error(t, blockTest("no block", func(ctx models.DBContext) (func() error, error){
return func() error{
return nil
}, nil
assert.Error(t, blockTest("no block", func(ctx models.DBContext) error {
return nil
}))

models.AssertNotExistsBean(t, &models.LockedResource{LockType: "test-1", LockKey: 1})

// Test with creation (i.e. new lock type)
assert.NoError(t, blockTest("block-new", func(ctx models.DBContext) (func() error, error){
assert.NoError(t, blockTest("block-new", func(ctx models.DBContext) error {
_, err := models.GetLockedResourceCtx(ctx, "block-test-1", 1)
return func() error{
return nil
}, err
return err
}))

// Test without creation (i.e. lock type already exists)
assert.NoError(t, blockTest("block-existing", func(ctx models.DBContext) (func() error, error){
assert.NoError(t, blockTest("block-existing", func(ctx models.DBContext) error {
_, err := models.GetLockedResourceCtx(ctx, "block-test-1", 1)
return func() error{
return nil
}, err
return err
}))

// Test with temporary record
assert.NoError(t, blockTest("block-temp", func(ctx models.DBContext) (func() error, error){
assert.NoError(t, blockTest("block-temp", func(ctx models.DBContext) error {
return models.TempLockResourceCtx(ctx, "temp-1", 1)
}))
}

func blockTest(name string, f func(ctx models.DBContext) (func() error, error)) error {
func blockTest(name string, f func(ctx models.DBContext) error) error {
cb := make(chan waitResult)
cw := make(chan waitResult)
ref := time.Now()
Expand All @@ -86,40 +84,39 @@ func blockTest(name string, f func(ctx models.DBContext) (func() error, error))
return resw.Err
}

if resw.Waited < after - tolerance {
if resw.Waited < blockerDelay - tolerance {
return fmt.Errorf("Waiter not blocked on %s; wait: %d ms, expected > %d ms",
name, resw.Waited.Milliseconds(), (after - tolerance).Milliseconds())
name, resw.Waited.Milliseconds(), (blockerDelay - tolerance).Milliseconds())
}

return nil
}

func blockTestFunc(name string, blocker bool, ref time.Time, f func(ctx models.DBContext) (func() error, error)) (wr waitResult) {
func blockTestFunc(name string, blocker bool, ref time.Time, f func(ctx models.DBContext) error) (wr waitResult) {
if blocker {
name = fmt.Sprintf("blocker [%s]", name)
} else {
name = fmt.Sprintf("waiter [%s]", name)
}
err := models.WithTx(func(ctx models.DBContext) error {
fmt.Printf("Entering %s @%d\n", name, time.Now().Sub(ref).Milliseconds())
log.Trace("Entering %s @%d", name, time.Now().Sub(ref).Milliseconds())
if !blocker {
fmt.Printf("Waiting on %s @%d\n", name, time.Now().Sub(ref).Milliseconds())
time.Sleep(before)
fmt.Printf("Wait finished on %s @%d\n", name, time.Now().Sub(ref).Milliseconds())
log.Trace("Waiting on %s @%d", name, time.Now().Sub(ref).Milliseconds())
time.Sleep(waiterDelay)
log.Trace("Wait finished on %s @%d", name, time.Now().Sub(ref).Milliseconds())
}
releaseLock, err := f(ctx)
if err != nil {
if err := f(ctx); err != nil {
return err
}
if blocker {
fmt.Printf("Waiting on %s @%d\n", name, time.Now().Sub(ref).Milliseconds())
time.Sleep(after)
fmt.Printf("Wait finished on %s @%d\n", name, time.Now().Sub(ref).Milliseconds())
log.Trace("Waiting on %s @%d", name, time.Now().Sub(ref).Milliseconds())
time.Sleep(blockerDelay)
log.Trace("Wait finished on %s @%d", name, time.Now().Sub(ref).Milliseconds())
} else {
wr.Waited = time.Now().Sub(ref)
}
fmt.Printf("Finishing %s @%d\n", name, time.Now().Sub(ref).Milliseconds())
return releaseLock()
log.Trace("Finishing %s @%d", name, time.Now().Sub(ref).Milliseconds())
return nil
})
if err != nil {
wr.Err = fmt.Errorf("error in %s: %v", name, err)
Expand Down
82 changes: 46 additions & 36 deletions models/locked_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,41 +6,30 @@ package models

import (
"fmt"

"code.gitea.io/gitea/modules/setting"
)

type LockedResource struct {
LockType string `xorm:"pk"`
LockType string `xorm:"pk VARCHAR(30)"`
LockKey int64 `xorm:"pk"`
Counter int64 `xorm:"NOT NULL DEFAULT 0"`
}

func GetLockedResource(e Engine, lockType string, lockKey int64) (*LockedResource, error) {
locked := &LockedResource{LockType: lockType, LockKey: lockKey}
// SQLite3 has no ForUpdate() clause and an UPSERT strategy has many
// problems and fallbacks; we perform a bogus update on the table
// which will lock the key in a safe way.
// Make sure to leave `counter` out of the update.
count, err := e.Table(locked).Cols("lock_type", "lock_key").Update(locked)
if err != nil {
return nil, fmt.Errorf("get locked resource %s:%d: %v", lockType, lockKey, err)

if err := upsertLockedResource(e, locked); err != nil {
return nil, fmt.Errorf("upsertLockedResource: %v", err)
}
if count == 0 {
// No record was found; since the key is now locked,
// it's safe to insert a record.
_, err = e.Insert(locked)
if err != nil {
return nil, fmt.Errorf("get locked resource %s:%d: %v", lockType, lockKey, err)
}
} else {
// Read back the record we've locked
has, err := e.Table(locked).Get(locked)
if err != nil {
return nil, fmt.Errorf("get locked resource %s:%d: %v", lockType, lockKey, err)
}
if !has {
return nil, fmt.Errorf("get locked resource %s:%d: record not found", lockType, lockKey)
}

// Read back the record we've created or locked to get the current Counter value
if has, err := e.Table(locked).Get(locked); err != nil {
return nil, fmt.Errorf("get locked resource %s:%d: %v", lockType, lockKey, err)
} else if !has {
return nil, fmt.Errorf("unexpected upsert fail %s:%d", lockType, lockKey)
}

return locked, nil
}

Expand All @@ -54,20 +43,15 @@ func DeleteLockedResource(e Engine, resource *LockedResource) error {
return err
}

func TempLockResource(e Engine, lockType string, lockKey int64) (func() error, error) {
func TempLockResource(e Engine, lockType string, lockKey int64) error {
locked := &LockedResource{LockType: lockType, LockKey: lockKey}
// Temporary locked resources must not exist in the table
// Temporary locked resources must not exist in the table.
// This allows us to use a simple INSERT to lock the key.
_, err := e.Insert(locked)
if err != nil {
return func() error {
return nil
},
fmt.Errorf("insert locked resource %s:%d: %v", lockType, lockKey, err)
if err == nil {
_, err = e.Delete(locked)
}
return func() error {
_, err := e.Delete(locked)
return err
}, nil
return err
}

func GetLockedResourceCtx(ctx DBContext, lockType string, lockKey int64) (*LockedResource, error) {
Expand All @@ -82,7 +66,33 @@ func DeleteLockedResourceCtx(ctx DBContext, resource *LockedResource) error {
return DeleteLockedResource(ctx.e, resource)
}

func TempLockResourceCtx(ctx DBContext, lockType string, lockKey int64) (func() error, error) {
func TempLockResourceCtx(ctx DBContext, lockType string, lockKey int64) error {
return TempLockResource(ctx.e, lockType, lockKey)
}

func upsertLockedResource(e Engine, resource *LockedResource) (err error) {
// An atomic UPSERT operation (INSERT/UPDATE) is the only operation
// that ensures that the key is actually locked.
switch {
case setting.Database.UseSQLite3 || setting.Database.UsePostgreSQL:
_, err = e.Exec("INSERT INTO locked_resource (lock_type, lock_key) "+
"VALUES (?,?) ON CONFLICT(lock_type, lock_key) DO UPDATE SET lock_key = ?",
resource.LockType, resource.LockKey, resource.LockKey);
case setting.Database.UseMySQL:
_, err = e.Exec("INSERT INTO locked_resource (lock_type, lock_key) "+
"VALUES (?,?) ON DUPLICATE KEY UPDATE lock_key = lock_key",
resource.LockType, resource.LockKey);
case setting.Database.UseMSSQL:
// https://weblogs.sqlteam.com/dang/2009/01/31/upsert-race-condition-with-merge/
_, err = e.Exec("MERGE locked_resource WITH (HOLDLOCK) as target "+
"USING (SELECT ? AS lock_type, ? AS lock_key) AS src "+
"ON src.lock_type = target.lock_type AND src.lock_key = target.lock_key "+
"WHEN MATCHED THEN UPDATE SET target.lock_key = target.lock_key "+
"WHEN NOT MATCHED THEN INSERT (lock_type, lock_key) "+
"VALUES (src.lock_type, src.lock_key);",
resource.LockType, resource.LockKey);
default:
return fmt.Errorf("database type not supported")
}
return
}
12 changes: 4 additions & 8 deletions models/locked_resource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ func TestLockedResource(t *testing.T) {

// Attempt temp lock on an existing key, expect error
withSession(t, func(t *testing.T, sess *xorm.Session) bool {
_, err := TempLockResource(sess, "test-1",1)
err := TempLockResource(sess, "test-1",1)
// Must give error
return assert.Error(t, err)
})

Expand All @@ -62,15 +63,10 @@ func TestLockedResource(t *testing.T) {

// Attempt temp lock on an valid key, expect success
withSession(t, func(t *testing.T, sess *xorm.Session) bool {
// Must give error
releaseLock, err := TempLockResource(sess, "test-1",1)
if !assert.NoError(t, err) {
return false
}
return assert.NoError(t, releaseLock())
return assert.NoError(t, TempLockResource(sess, "test-1",1))
})

// Note: testing the validity of the locking mechanism (i.e. whether it actually locks)
// must be done in the integration tests, so all the supported databases are checked.
// is be done at the integration tests to ensure that all the supported databases are checked.
}

2 changes: 1 addition & 1 deletion models/migrations/v125.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (
func addLockedResourceTable(x *xorm.Engine) error {

type LockedResource struct {
LockType string `xorm:"pk"`
LockType string `xorm:"pk VARCHAR(30)"`
LockKey int64 `xorm:"pk"`
Counter int64 `xorm:"NOT NULL DEFAULT 0"`
}
Expand Down