Index: appengine/memlock/memlock_test.go |
diff --git a/appengine/memlock/memlock_test.go b/appengine/memlock/memlock_test.go |
new file mode 100644 |
index 0000000000000000000000000000000000000000..4b9893daffd03154acad81e2c6c79f677fc115eb |
--- /dev/null |
+++ b/appengine/memlock/memlock_test.go |
@@ -0,0 +1,188 @@ |
+// Copyright 2015 The Chromium Authors. All rights reserved. |
+// Use of this source code is governed by a BSD-style license that can be |
+// found in the LICENSE file. |
+ |
+package memlock |
+ |
+import ( |
+ "fmt" |
+ "sync" |
+ "testing" |
+ "time" |
+ |
+ "github.com/luci/gae/filter/featureBreaker" |
+ "github.com/luci/gae/impl/memory" |
+ "github.com/luci/gae/service/memcache" |
+ "github.com/luci/luci-go/common/clock" |
+ "github.com/luci/luci-go/common/clock/testclock" |
+ . "github.com/smartystreets/goconvey/convey" |
+ "golang.org/x/net/context" |
+) |
+ |
+type getBlockerFilter struct { |
+ memcache.RawInterface |
+ sync.Mutex |
+ |
+ dropAll bool |
+} |
+ |
+func (f *getBlockerFilter) GetMulti(keys []string, cb memcache.RawItemCB) error { |
+ f.Lock() |
+ defer f.Unlock() |
+ if f.dropAll { |
+ for _, key := range keys { |
+ cb(f.NewItem(key), nil) |
+ } |
+ return nil |
+ } |
+ return f.RawInterface.GetMulti(keys, cb) |
+} |
+ |
+func TestSimple(t *testing.T) { |
+ // TODO(riannucci): Mock time.After so that we don't have to delay for real. |
+ |
+ const key = memlockKeyPrefix + "testkey" |
+ |
+ Convey("basic locking", t, func() { |
+ start := time.Date(1986, time.October, 26, 1, 20, 00, 00, time.UTC) |
+ ctx, clk := testclock.UseTime(context.Background(), start) |
+ blocker := make(chan struct{}) |
+ clk.SetTimerCallback(func(time.Duration, clock.Timer) { |
+ clk.Add(delay) |
+ select { |
+ case blocker <- struct{}{}: |
+ default: |
+ } |
+ }) |
+ |
+ waitFalse := func(ctx context.Context) { |
+ loop: |
+ for { |
+ select { |
+ case <-blocker: |
+ continue |
+ case <-ctx.Done(): |
+ break loop |
+ } |
+ } |
+ } |
+ |
+ ctx, fb := featureBreaker.FilterMC(memory.Use(ctx), nil) |
+ mc := memcache.Get(ctx) |
+ |
+ Convey("fails to acquire when memcache is down", func() { |
+ fb.BreakFeatures(nil, "AddMulti") |
+ err := TryWithLock(ctx, "testkey", "id", func(context.Context) error { |
+ // should never reach here |
+ So(false, ShouldBeTrue) |
+ return nil |
+ }) |
+ So(err, ShouldEqual, ErrFailedToLock) |
+ }) |
+ |
+ Convey("returns the inner error", func() { |
+ toRet := fmt.Errorf("sup") |
+ err := TryWithLock(ctx, "testkey", "id", func(context.Context) error { |
+ return toRet |
+ }) |
+ So(err, ShouldEqual, toRet) |
+ }) |
+ |
+ Convey("returns the error", func() { |
+ toRet := fmt.Errorf("sup") |
+ err := TryWithLock(ctx, "testkey", "id", func(context.Context) error { |
+ return toRet |
+ }) |
+ So(err, ShouldEqual, toRet) |
+ }) |
+ |
+ Convey("can acquire when empty", func() { |
+ err := TryWithLock(ctx, "testkey", "id", func(ctx context.Context) error { |
+ isDone := func() bool { |
+ select { |
+ case <-ctx.Done(): |
+ return true |
+ default: |
+ return false |
+ } |
+ } |
+ |
+ So(isDone(), ShouldBeFalse) |
+ |
+ Convey("waiting for a while keeps refreshing the lock", func() { |
+ // simulate waiting for 64*delay time, and ensuring that checkLoop |
+ // runs that many times. |
+ for i := 0; i < 64; i++ { |
+ <-blocker |
+ clk.Add(delay) |
+ } |
+ So(isDone(), ShouldBeFalse) |
+ }) |
+ |
+ Convey("but sometimes we might lose it", func() { |
+ Convey("because it was evicted", func() { |
+ mc.Delete(key) |
+ clk.Add(memcacheLockTime) |
+ waitFalse(ctx) |
+ }) |
+ |
+ Convey("or because of service issues", func() { |
+ fb.BreakFeatures(nil, "CompareAndSwapMulti") |
+ waitFalse(ctx) |
+ }) |
+ }) |
+ return nil |
+ }) |
+ So(err, ShouldBeNil) |
+ }) |
+ |
+ Convey("can lose it when it gets stolen", func() { |
+ gbf := &getBlockerFilter{} |
+ ctx = memcache.AddRawFilters(ctx, func(_ context.Context, mc memcache.RawInterface) memcache.RawInterface { |
+ gbf.RawInterface = mc |
+ return gbf |
+ }) |
+ mc = memcache.Get(ctx) |
+ err := TryWithLock(ctx, "testkey", "id", func(ctx context.Context) error { |
+ // simulate waiting for 64*delay time, and ensuring that checkLoop |
+ // runs that many times. |
+ for i := 0; i < 64; i++ { |
+ <-blocker |
+ clk.Add(delay) |
+ } |
+ gbf.Lock() |
+ mc.Set(mc.NewItem(key).SetValue([]byte("wat"))) |
+ gbf.Unlock() |
+ waitFalse(ctx) |
+ return nil |
+ }) |
+ So(err, ShouldBeNil) |
+ }) |
+ |
+ Convey("can lose it when it gets preemptively released", func() { |
+ gbf := &getBlockerFilter{} |
+ ctx = memcache.AddRawFilters(ctx, func(_ context.Context, mc memcache.RawInterface) memcache.RawInterface { |
+ gbf.RawInterface = mc |
+ return gbf |
+ }) |
+ ctx = context.WithValue(ctx, testStopCBKey, func() { |
+ gbf.dropAll = true |
+ }) |
+ mc = memcache.Get(ctx) |
+ err := TryWithLock(ctx, "testkey", "id", func(ctx context.Context) error { |
+ // simulate waiting for 64*delay time, and ensuring that checkLoop |
+ // runs that many times. |
+ for i := 0; i < 64; i++ { |
+ <-blocker |
+ clk.Add(delay) |
+ } |
+ return nil |
+ }) |
+ So(err, ShouldBeNil) |
+ }) |
+ |
+ Convey("an empty context id is an error", func() { |
+ So(TryWithLock(ctx, "testkey", "", nil), ShouldEqual, ErrEmptyClientID) |
+ }) |
+ }) |
+} |