go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/scheduler/appengine/engine/policy/newest_first_test.go (about) 1 // Copyright 2023 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 policy 16 17 import ( 18 "fmt" 19 "testing" 20 "time" 21 22 "go.chromium.org/luci/scheduler/appengine/internal" 23 "go.chromium.org/luci/scheduler/appengine/task" 24 25 . "github.com/smartystreets/goconvey/convey" 26 ) 27 28 func TestNewestFirst(t *testing.T) { 29 t.Parallel() 30 31 Convey("With simulator", t, func(c C) { 32 invocationDuration := time.Hour // may be modified in tests below. 33 s := Simulator{ 34 OnRequest: func(s *Simulator, r task.Request) time.Duration { 35 return invocationDuration 36 }, 37 OnDebugLog: func(format string, args ...any) { 38 _, _ = c.Printf(format+"\n", args...) 39 }, 40 } 41 42 const noDelay = time.Duration(0) 43 lastAddedTrigger := 0 44 addTriggers := func(delay time.Duration, n int) { 45 ts := make([]internal.Trigger, n) 46 for i := range ts { 47 lastAddedTrigger++ 48 ts[i] = internal.NoopTrigger( 49 fmt.Sprintf("t-%03d", lastAddedTrigger), 50 fmt.Sprintf("data-%03d", lastAddedTrigger), 51 ) 52 } 53 s.AddTrigger(delay, ts...) 54 } 55 56 var err error 57 58 Convey("Pending timeout must be positive", func() { 59 s.Policy, err = NewestFirstPolicy(2, -time.Hour) 60 So(err, ShouldNotBeNil) 61 }) 62 63 Convey("Newest first works", func() { 64 Convey("at least 1 trigger", func() { 65 s.Policy, err = NewestFirstPolicy(1, 300*invocationDuration) 66 So(err, ShouldBeNil) 67 68 // Add exactly one trigger. 69 addTriggers(noDelay, 1) 70 So(s.Invocations, ShouldHaveLength, 1) 71 So(s.Last().Request.TriggerIDs(), ShouldResemble, []string{"t-001"}) 72 So(s.Last().Request.StringProperty("noop_trigger_data"), ShouldEqual, "data-001") 73 So(s.PendingTriggers, ShouldHaveLength, 0) 74 So(s.DiscardedTriggers, ShouldHaveLength, 0) 75 }) 76 77 Convey("process newer triggers first", func() { 78 s.Policy, err = NewestFirstPolicy(1, 300*invocationDuration) 79 So(err, ShouldBeNil) 80 81 const N = 3 82 addTriggers(noDelay, N) 83 for i := N - 1; i >= 0; i-- { 84 So(s.Invocations, ShouldHaveLength, N-i) 85 So(s.Last().Request.TriggerIDs(), ShouldResemble, []string{fmt.Sprintf("t-%03d", i+1)}) 86 So(s.Last().Request.StringProperty("noop_trigger_data"), ShouldEqual, fmt.Sprintf("data-%03d", i+1)) 87 So(s.PendingTriggers, ShouldHaveLength, i) 88 So(s.DiscardedTriggers, ShouldHaveLength, 0) 89 addTriggers(invocationDuration, 0) 90 } 91 So(s.Invocations, ShouldHaveLength, N) 92 So(s.DiscardedTriggers, ShouldHaveLength, 0) 93 }) 94 95 Convey("respects pending timeout", func() { 96 const N = 2 97 const extra = 2 98 s.Policy, err = NewestFirstPolicy(1, N*invocationDuration) 99 So(err, ShouldBeNil) 100 101 // Add more extra triggers than we can fit running serially in the pending timeout. 102 // This extra trigger will be discarded. 103 addTriggers(noDelay, N+extra) 104 105 So(s.Invocations, ShouldHaveLength, 1) 106 So(s.Last().Request.TriggerIDs(), ShouldResemble, []string{"t-004"}) 107 So(s.Last().Request.StringProperty("noop_trigger_data"), ShouldEqual, "data-004") 108 So(s.PendingTriggers, ShouldHaveLength, 1+extra) 109 So(s.DiscardedTriggers, ShouldHaveLength, 0) 110 111 // Advance time, allowing an invocation to finish. 112 addTriggers(invocationDuration, 0) 113 114 So(s.Invocations, ShouldHaveLength, 2) 115 So(s.Last().Request.TriggerIDs(), ShouldResemble, []string{"t-003"}) 116 So(s.Last().Request.StringProperty("noop_trigger_data"), ShouldEqual, "data-003") 117 So(s.PendingTriggers, ShouldHaveLength, extra) 118 So(s.DiscardedTriggers, ShouldHaveLength, 0) 119 120 // Advance time, allowing an invocation to finish. 121 addTriggers(invocationDuration, 0) 122 123 So(s.Invocations, ShouldHaveLength, N) 124 So(s.DiscardedTriggers, ShouldHaveLength, extra) 125 }) 126 127 Convey("pending timeout discards triggers due to starvation", func() { 128 const N = 3 129 s.Policy, err = NewestFirstPolicy(1, N*invocationDuration) 130 So(err, ShouldBeNil) 131 132 // Add more extra triggers than we can fit running serially in the pending timeout. 133 // This extra trigger will be discarded. 134 extra := 1 135 addTriggers(noDelay, 1+extra) 136 for i := 0; i < N; i++ { 137 So(s.Invocations, ShouldHaveLength, i+1) 138 So(s.Last().Request.TriggerIDs(), ShouldResemble, []string{fmt.Sprintf("t-%03d", i+1+extra)}) 139 So(s.Last().Request.StringProperty("noop_trigger_data"), ShouldEqual, fmt.Sprintf("data-%03d", i+1+extra)) 140 So(s.PendingTriggers, ShouldHaveLength, 1) 141 So(s.DiscardedTriggers, ShouldHaveLength, 0) 142 143 // Add the trigger first. We want a newer trigger to always come in before a slot frees up so 144 // that the extra triggers get starved. 145 addTriggers(invocationDuration/2, 1) 146 addTriggers(invocationDuration/2, 0) 147 } 148 So(s.Invocations, ShouldHaveLength, N+1) 149 So(s.DiscardedTriggers, ShouldHaveLength, extra) 150 }) 151 152 Convey("very short timeout causes immediate discard", func() { 153 s.Policy, err = NewestFirstPolicy(1, time.Nanosecond) 154 So(err, ShouldBeNil) 155 156 for i := 0; i < 3; i++ { 157 addTriggers(invocationDuration, 2) 158 So(s.Invocations, ShouldHaveLength, i+1) 159 So(s.PendingTriggers, ShouldHaveLength, 1) 160 So(s.DiscardedTriggers, ShouldHaveLength, i) 161 } 162 }) 163 164 Convey("multiple concurrent invocations", func() { 165 const concurrentInvocations = 2 166 s.Policy, err = NewestFirstPolicy(concurrentInvocations, 2*invocationDuration) 167 So(err, ShouldBeNil) 168 169 addTriggers(noDelay, 6) 170 171 So(s.Invocations, ShouldHaveLength, 2) 172 So(s.PendingTriggers, ShouldHaveLength, 4) 173 So(s.DiscardedTriggers, ShouldHaveLength, 0) 174 175 addTriggers(invocationDuration, 2) 176 177 So(s.Invocations, ShouldHaveLength, 4) 178 So(s.PendingTriggers, ShouldHaveLength, 4) 179 So(s.DiscardedTriggers, ShouldHaveLength, 0) 180 181 addTriggers(invocationDuration, 2) 182 183 So(s.Invocations, ShouldHaveLength, 6) 184 So(s.PendingTriggers, ShouldHaveLength, 2) 185 So(s.DiscardedTriggers, ShouldHaveLength, 2) 186 }) 187 }) 188 }) 189 }