go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/scheduler/appengine/engine/utils_test.go (about) 1 // Copyright 2017 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package engine 16 17 import ( 18 "context" 19 "testing" 20 21 "go.chromium.org/luci/common/clock" 22 "go.chromium.org/luci/common/clock/testclock" 23 "go.chromium.org/luci/common/errors" 24 "go.chromium.org/luci/common/retry/transient" 25 "go.chromium.org/luci/gae/impl/memory" 26 "go.chromium.org/luci/gae/service/datastore" 27 28 . "github.com/smartystreets/goconvey/convey" 29 ) 30 31 func TestRunTxn(t *testing.T) { 32 t.Parallel() 33 34 Convey("With mock context", t, func(C) { 35 c := memory.Use(context.Background()) 36 c = clock.Set(c, testclock.New(epoch)) 37 38 Convey("Happy path", func() { 39 calls := 0 40 err := runTxn(c, func(ctx context.Context) error { 41 calls++ 42 job := Job{JobID: "123", Revision: "abc"} 43 inner := datastore.Put(ctx, &job) 44 So(inner, ShouldBeNil) 45 return nil 46 }) 47 So(err, ShouldBeNil) 48 So(calls, ShouldEqual, 1) // one successful attempt 49 50 // Committed. 51 job := Job{JobID: "123"} 52 So(datastore.Get(c, &job), ShouldBeNil) 53 So(job.Revision, ShouldEqual, "abc") 54 }) 55 56 Convey("Transient error", func() { 57 calls := 0 58 transient := errors.New("transient error", transient.Tag) 59 err := runTxn(c, func(ctx context.Context) error { 60 calls++ 61 job := Job{JobID: "123", Revision: "abc"} 62 inner := datastore.Put(ctx, &job) 63 So(inner, ShouldBeNil) 64 return transient 65 }) 66 So(err, ShouldEqual, transient) 67 So(calls, ShouldEqual, defaultTransactionOptions.Attempts) // all attempts 68 69 // Not committed. 70 job := Job{JobID: "123"} 71 So(datastore.Get(c, &job), ShouldEqual, datastore.ErrNoSuchEntity) 72 }) 73 74 Convey("Fatal error", func() { 75 calls := 0 76 fatal := errors.New("fatal error") 77 err := runTxn(c, func(ctx context.Context) error { 78 calls++ 79 job := Job{JobID: "123", Revision: "abc"} 80 inner := datastore.Put(ctx, &job) 81 So(inner, ShouldBeNil) 82 return fatal 83 }) 84 So(err, ShouldEqual, fatal) 85 So(calls, ShouldEqual, 1) // one failed attempt 86 87 // Not committed. 88 job := Job{JobID: "123"} 89 So(datastore.Get(c, &job), ShouldEqual, datastore.ErrNoSuchEntity) 90 }) 91 92 Convey("Transient error, but marked as abortTransaction", func() { 93 calls := 0 94 transient := errors.New("transient error", transient.Tag, abortTransaction) 95 err := runTxn(c, func(ctx context.Context) error { 96 calls++ 97 job := Job{JobID: "123", Revision: "abc"} 98 inner := datastore.Put(ctx, &job) 99 So(inner, ShouldBeNil) 100 return transient 101 }) 102 So(err, ShouldEqual, transient) 103 So(calls, ShouldEqual, 1) // one failed attempt 104 105 // Not committed. 106 job := Job{JobID: "123"} 107 So(datastore.Get(c, &job), ShouldEqual, datastore.ErrNoSuchEntity) 108 }) 109 }) 110 } 111 112 func TestOpsCache(t *testing.T) { 113 t.Parallel() 114 115 Convey("Works", t, func(C) { 116 c := memory.Use(context.Background()) 117 118 calls := 0 119 cb := func() error { 120 calls++ 121 return nil 122 } 123 124 ops := opsCache{} 125 So(ops.Do(c, "key", cb), ShouldBeNil) 126 So(calls, ShouldEqual, 1) 127 128 // Second call is skipped. 129 So(ops.Do(c, "key", cb), ShouldBeNil) 130 So(calls, ShouldEqual, 1) 131 132 // Make sure memcache-based deduplication also works. 133 ops.doneFlags = nil 134 So(ops.Do(c, "key", cb), ShouldBeNil) 135 So(calls, ShouldEqual, 1) 136 }) 137 }