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
A little refactoring and better function naming
  • Loading branch information
guillep2k committed Jan 22, 2020
commit d8ad17445ee3a1cc1f16646a7881ffedf1d50e5b
2 changes: 1 addition & 1 deletion integrations/locked_resource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ func TestLockedResource(t *testing.T) {

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

Expand Down
10 changes: 5 additions & 5 deletions models/issue.go
Original file line number Diff line number Diff line change
Expand Up @@ -905,15 +905,15 @@ func newIssue(e *xorm.Session, doer *User, opts NewIssueOptions) (err error) {
// Obtain the next issue number for this repository, which will be locked
// and reserved for the remaining of the transaction. Should the transaction
// be rolled back, the previous value will be restored.
locked, err := GetLockedResource(e, IssueLockedEnumerator, opts.Issue.RepoID)
idxresource, err := GetLockedResource(e, IssueLockedEnumerator, opts.Issue.RepoID)
if err != nil {
return fmt.Errorf("GetLockedResource(%s)", IssueLockedEnumerator)
}
locked.Counter++
if err := UpdateLockedResource(e, locked); err != nil {
return fmt.Errorf("UpdateLockedResource(%s)", IssueLockedEnumerator)
idxresource.Counter++
if err := idxresource.UpdateValue(); err != nil {
return fmt.Errorf("locked.UpdateValue(%s)", IssueLockedEnumerator)
}
opts.Issue.Index = locked.Counter
opts.Issue.Index = idxresource.Counter

if _, err = e.Insert(opts.Issue); err != nil {
return err
Expand Down
8 changes: 4 additions & 4 deletions models/issue_xref_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,11 +130,11 @@ func testCreateIssue(t *testing.T, repo, doer int64, title, content string, ispu
sess := x.NewSession()
defer sess.Close()
assert.NoError(t, sess.Begin())
lock, err := GetLockedResource(sess, IssueLockedEnumerator, repo)
idxresource, err := GetLockedResource(sess, IssueLockedEnumerator, repo)
assert.NoError(t, err)
lock.Counter++
assert.NoError(t, UpdateLockedResource(sess, lock))
i.Index = lock.Counter
idxresource.Counter++
assert.NoError(t, idxresource.UpdateValue())
i.Index = idxresource.Counter
_, err = sess.Insert(i)
assert.NoError(t, err)
assert.NoError(t, i.addCrossReferences(sess, d, false))
Expand Down
61 changes: 34 additions & 27 deletions models/locked_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,46 +16,58 @@ type LockedResource struct {
LockType string `xorm:"pk VARCHAR(30)"`
LockKey int64 `xorm:"pk"`
Counter int64 `xorm:"NOT NULL DEFAULT 0"`

engine Engine `xorm:"-"`
}

// GetLockedResource gets or creates a pessimistic lock on the given type and key
func GetLockedResource(e Engine, lockType string, lockKey int64) (*LockedResource, error) {
locked := &LockedResource{LockType: lockType, LockKey: lockKey}
resource := &LockedResource{LockType: lockType, LockKey: lockKey}

if err := upsertLockedResource(e, locked); err != nil {
if err := upsertLockedResource(e, resource); err != nil {
return nil, fmt.Errorf("upsertLockedResource: %v", err)
}

// 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 {
if has, err := e.Table(resource).Get(resource); 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
// Once active, the locked resource is tied to a specific session
resource.engine = e

return resource, nil
}

// UpdateLockedResource updates the value of the counter of a locked resource
func UpdateLockedResource(e Engine, resource *LockedResource) error {
_, err := e.Table(resource).Cols("counter").Update(resource)
// UpdateValue updates the value of the counter of a locked resource
func (r *LockedResource) UpdateValue() error {
_, err := r.engine.Table(r).Cols("counter").Update(r)
return err
}

// DeleteLockedResource deletes a locked resource
func DeleteLockedResource(e Engine, resource *LockedResource) error {
_, err := e.Delete(resource)
// Delete deletes the locked resource from the database,
// but the key remains locked until the end of the transaction
func (r *LockedResource) Delete() error {
_, err := r.engine.Delete(r)
return err
}

// TempLockResource locks the given key but does not leave a permanent record
func TempLockResource(e Engine, lockType string, lockKey int64) error {
locked := &LockedResource{LockType: lockType, LockKey: lockKey}
// Temporary locked resources must not exist in the table.
// DeleteLockedResourceKey deletes a locked resource by key
func DeleteLockedResourceKey(e Engine, lockType string, lockKey int64) error {
_, err := e.Delete(&LockedResource{LockType: lockType, LockKey: lockKey})
return err
}

// TemporarilyLockResourceKey locks the given key but does not leave a permanent record
func TemporarilyLockResourceKey(e Engine, lockType string, lockKey int64) error {
resource := &LockedResource{LockType: lockType, LockKey: lockKey}
// Temporary locked resources should not exist in the table.
// This allows us to use a simple INSERT to lock the key.
_, err := e.Insert(locked)
_, err := e.Insert(resource)
if err == nil {
_, err = e.Delete(locked)
_, err = e.Delete(resource)
}
return err
}
Expand All @@ -65,19 +77,14 @@ func GetLockedResourceCtx(ctx DBContext, lockType string, lockKey int64) (*Locke
return GetLockedResource(ctx.e, lockType, lockKey)
}

// UpdateLockedResourceCtx updates the value of the counter of a locked resource
func UpdateLockedResourceCtx(ctx DBContext, resource *LockedResource) error {
return UpdateLockedResource(ctx.e, resource)
}

// DeleteLockedResourceCtx deletes a locked resource
func DeleteLockedResourceCtx(ctx DBContext, resource *LockedResource) error {
return DeleteLockedResource(ctx.e, resource)
// DeleteLockedResourceKeyCtx deletes a locked resource by key
func DeleteLockedResourceKeyCtx(ctx DBContext, lockType string, lockKey int64) error {
return DeleteLockedResourceKey(ctx.e, lockType, lockKey)
}

// TempLockResourceCtx locks the given key but does not leave a permanent record
func TempLockResourceCtx(ctx DBContext, lockType string, lockKey int64) error {
return TempLockResource(ctx.e, lockType, lockKey)
// TemporarilyLockResourceKeyCtx locks the given key but does not leave a permanent record
func TemporarilyLockResourceKeyCtx(ctx DBContext, lockType string, lockKey int64) error {
return TemporarilyLockResourceKey(ctx.e, lockType, lockKey)
}

// upsertLockedResource will create or lock the given key in the database.
Expand Down
34 changes: 23 additions & 11 deletions models/locked_resource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,40 +30,52 @@ func TestLockedResource(t *testing.T) {

// Get lock, increment counter value
withSession(t, func(t *testing.T, sess *xorm.Session) bool {
lck1, err := GetLockedResource(sess, "test-1", 1)
if !assert.NoError(t, err) || !assert.NotEmpty(t, lck1) || !assert.Equal(t, int64(0), lck1.Counter) {
resource, err := GetLockedResource(sess, "test-1", 1)
if !assert.NoError(t, err) || !assert.NotEmpty(t, resource) || !assert.Equal(t, int64(0), resource.Counter) {
return false
}
lck1.Counter++
err = UpdateLockedResource(sess, lck1)
resource.Counter++
err = resource.UpdateValue()
return assert.NoError(t, err)
})

// Get lock, check counter value
withSession(t, func(t *testing.T, sess *xorm.Session) bool {
lck1, err := GetLockedResource(sess, "test-1", 1)
return assert.NoError(t, err) && assert.NotEmpty(t, lck1) && assert.Equal(t, int64(1), lck1.Counter)
resource, err := GetLockedResource(sess, "test-1", 1)
return assert.NoError(t, err) && assert.NotEmpty(t, resource) && assert.Equal(t, int64(1), resource.Counter)
})

// 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 := TemporarilyLockResourceKey(sess, "test-1", 1)
// Must give error
return assert.Error(t, err)
})

// Delete lock
withSession(t, func(t *testing.T, sess *xorm.Session) bool {
lck1, err := GetLockedResource(sess, "test-1", 1)
if !assert.NoError(t, err) || !assert.NotEmpty(t, lck1) {
resource, err := GetLockedResource(sess, "test-1", 1)
if !assert.NoError(t, err) || !assert.NotEmpty(t, resource) {
return false
}
return assert.NoError(t, DeleteLockedResource(sess, lck1))
return assert.NoError(t, resource.Delete())
})
AssertNotExistsBean(t, &LockedResource{LockType: "test-1", LockKey: 1})

// Get lock, then delete by key
withSession(t, func(t *testing.T, sess *xorm.Session) bool {
resource, err := GetLockedResource(sess, "test-1", 2)
return assert.NoError(t, err) && assert.NotEmpty(t, resource)
})
AssertExistsAndLoadBean(t, &LockedResource{LockType: "test-1", LockKey: 2})
withSession(t, func(t *testing.T, sess *xorm.Session) bool {
return assert.NoError(t, DeleteLockedResourceKey(sess, "test-1", 2))
})
AssertNotExistsBean(t, &LockedResource{LockType: "test-1", LockKey: 2})

// Attempt temp lock on an valid key, expect success
withSession(t, func(t *testing.T, sess *xorm.Session) bool {
return assert.NoError(t, TempLockResource(sess, "test-1", 1))
return assert.NoError(t, TemporarilyLockResourceKey(sess, "test-1", 1))
})

// Note: testing the validity of the locking mechanism (i.e. whether it actually locks)
Expand Down