go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cv/internal/tryjob/execute/retry_test.go (about) 1 // Copyright 2022 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 execute 16 17 import ( 18 "context" 19 "math" 20 "testing" 21 22 "google.golang.org/protobuf/types/known/timestamppb" 23 24 "go.chromium.org/luci/common/clock" 25 "go.chromium.org/luci/common/clock/testclock" 26 27 cfgpb "go.chromium.org/luci/cv/api/config/v2" 28 "go.chromium.org/luci/cv/api/recipe/v1" 29 "go.chromium.org/luci/cv/internal/tryjob" 30 31 . "github.com/smartystreets/goconvey/convey" 32 . "go.chromium.org/luci/common/testing/assertions" 33 ) 34 35 func TestCanRetryAll(t *testing.T) { 36 Convey("CanRetryAll", t, func() { 37 ctx, _ := testclock.UseTime(context.Background(), testclock.TestRecentTimeUTC) 38 const builderZero = "builder-zero" 39 const builderOne = "builder-one" 40 execState := newExecStateBuilder(). 41 appendDefinition(makeDefinition(builderZero, true)). 42 appendDefinition(makeDefinition(builderOne, true)). 43 withRetryConfig(&cfgpb.Verifiers_Tryjob_RetryConfig{ 44 SingleQuota: 2, 45 GlobalQuota: 3, 46 FailureWeight: 3, 47 TransientFailureWeight: 1, 48 TimeoutWeight: 2, 49 }). 50 build() 51 executor := &Executor{} 52 53 Convey("With no retry config", func() { 54 Convey("Nil config", func() { 55 execState = newExecStateBuilder(execState). 56 withRetryConfig(nil). 57 appendAttempt(builderZero, makeAttempt(1, tryjob.Status_ENDED, tryjob.Result_FAILED_TRANSIENTLY)). 58 build() 59 }) 60 Convey("Empty config", func() { 61 execState = newExecStateBuilder(execState). 62 withRetryConfig(&cfgpb.Verifiers_Tryjob_RetryConfig{}). 63 appendAttempt(builderZero, makeAttempt(1, tryjob.Status_ENDED, tryjob.Result_FAILED_TRANSIENTLY)). 64 build() 65 }) 66 ok := executor.canRetryAll(ctx, execState, []int{0}) 67 So(ok, ShouldBeFalse) 68 So(executor.logEntries, ShouldResembleProto, []*tryjob.ExecutionLogEntry{ 69 { 70 Time: timestamppb.New(clock.Now(ctx).UTC()), 71 Kind: &tryjob.ExecutionLogEntry_RetryDenied_{ 72 RetryDenied: &tryjob.ExecutionLogEntry_RetryDenied{ 73 Tryjobs: []*tryjob.ExecutionLogEntry_TryjobSnapshot{ 74 { 75 Definition: makeDefinition(builderZero, true), 76 Id: 1, 77 ExternalId: string(tryjob.MustBuildbucketID("buildbucket.example.com", math.MaxInt64-1)), 78 Status: tryjob.Status_ENDED, 79 Result: &tryjob.Result{ 80 Status: tryjob.Result_FAILED_TRANSIENTLY, 81 }, 82 }, 83 }, 84 Reason: "retry is not enabled in the config", 85 }, 86 }, 87 }, 88 }) 89 }) 90 Convey("With retry config", func() { 91 Convey("quota", func() { 92 Convey("allows retry", func() { 93 execState = newExecStateBuilder(execState). 94 appendAttempt(builderZero, makeAttempt(345, tryjob.Status_ENDED, tryjob.Result_FAILED_TRANSIENTLY)). 95 build() 96 ok := executor.canRetryAll(ctx, execState, []int{0}) 97 So(ok, ShouldBeTrue) 98 So(executor.logEntries, ShouldBeEmpty) 99 }) 100 Convey("for single execution exceeded", func() { 101 execState = newExecStateBuilder(execState). 102 appendAttempt(builderZero, makeAttempt(345, tryjob.Status_ENDED, tryjob.Result_FAILED_PERMANENTLY)). 103 build() 104 ok := executor.canRetryAll(ctx, execState, []int{0}) 105 So(ok, ShouldBeFalse) 106 So(execState.GetExecutions()[0].GetUsedQuota(), ShouldEqual, 3) 107 So(executor.logEntries, ShouldResembleProto, []*tryjob.ExecutionLogEntry{ 108 { 109 Time: timestamppb.New(clock.Now(ctx).UTC()), 110 Kind: &tryjob.ExecutionLogEntry_RetryDenied_{ 111 RetryDenied: &tryjob.ExecutionLogEntry_RetryDenied{ 112 Tryjobs: []*tryjob.ExecutionLogEntry_TryjobSnapshot{ 113 { 114 Definition: makeDefinition(builderZero, true), 115 Id: 345, 116 ExternalId: string(tryjob.MustBuildbucketID("buildbucket.example.com", math.MaxInt64-345)), 117 Status: tryjob.Status_ENDED, 118 Result: &tryjob.Result{ 119 Status: tryjob.Result_FAILED_PERMANENTLY, 120 }, 121 }, 122 }, 123 Reason: "insufficient quota", 124 }, 125 }, 126 }, 127 }) 128 }) 129 Convey("for whole run exceeded", func() { 130 execState = newExecStateBuilder(execState). 131 appendAttempt(builderZero, makeAttempt(345, tryjob.Status_ENDED, tryjob.Result_TIMEOUT)). 132 appendAttempt(builderOne, makeAttempt(567, tryjob.Status_ENDED, tryjob.Result_TIMEOUT)). 133 build() 134 ok := executor.canRetryAll(ctx, execState, []int{0, 1}) 135 So(ok, ShouldBeFalse) 136 So(execState.GetExecutions()[0].GetUsedQuota(), ShouldEqual, 2) 137 So(execState.GetExecutions()[1].GetUsedQuota(), ShouldEqual, 2) 138 So(executor.logEntries, ShouldResembleProto, []*tryjob.ExecutionLogEntry{ 139 { 140 Time: timestamppb.New(clock.Now(ctx).UTC()), 141 Kind: &tryjob.ExecutionLogEntry_RetryDenied_{ 142 RetryDenied: &tryjob.ExecutionLogEntry_RetryDenied{ 143 Tryjobs: []*tryjob.ExecutionLogEntry_TryjobSnapshot{ 144 { 145 Definition: makeDefinition(builderZero, true), 146 Id: 345, 147 ExternalId: string(tryjob.MustBuildbucketID("buildbucket.example.com", math.MaxInt64-345)), 148 Status: tryjob.Status_ENDED, 149 Result: &tryjob.Result{ 150 Status: tryjob.Result_TIMEOUT, 151 }, 152 }, 153 { 154 Definition: makeDefinition(builderOne, true), 155 Id: 567, 156 ExternalId: string(tryjob.MustBuildbucketID("buildbucket.example.com", math.MaxInt64-567)), 157 Status: tryjob.Status_ENDED, 158 Result: &tryjob.Result{ 159 Status: tryjob.Result_TIMEOUT, 160 }, 161 }, 162 }, 163 Reason: "insufficient global quota", 164 }, 165 }, 166 }, 167 }) 168 }) 169 Convey("reused run does not cause exceeding", func() { 170 execState = newExecStateBuilder(execState). 171 appendAttempt(builderZero, makeAttempt(345, tryjob.Status_ENDED, tryjob.Result_FAILED_PERMANENTLY)). 172 build() 173 execState.GetExecutions()[0].Attempts[0].Reused = true 174 ok := executor.canRetryAll(ctx, execState, []int{0}) 175 So(ok, ShouldBeTrue) 176 So(execState.GetExecutions()[0].GetUsedQuota(), ShouldEqual, 0) 177 So(executor.logEntries, ShouldBeEmpty) 178 }) 179 Convey("with retry-denied property output", func() { 180 execState = newExecStateBuilder(execState). 181 appendAttempt(builderZero, makeAttempt(345, tryjob.Status_ENDED, tryjob.Result_FAILED_TRANSIENTLY)). 182 build() 183 execState.GetExecutions()[0].Attempts[0].Result.Output = &recipe.Output{Retry: recipe.Output_OUTPUT_RETRY_DENIED} 184 ok := executor.canRetryAll(ctx, execState, []int{0}) 185 So(ok, ShouldBeFalse) 186 So(executor.logEntries, ShouldResembleProto, []*tryjob.ExecutionLogEntry{ 187 { 188 Time: timestamppb.New(clock.Now(ctx).UTC()), 189 Kind: &tryjob.ExecutionLogEntry_RetryDenied_{ 190 RetryDenied: &tryjob.ExecutionLogEntry_RetryDenied{ 191 Tryjobs: []*tryjob.ExecutionLogEntry_TryjobSnapshot{ 192 { 193 Definition: makeDefinition(builderZero, true), 194 Id: 345, 195 ExternalId: string(tryjob.MustBuildbucketID("buildbucket.example.com", math.MaxInt64-345)), 196 Status: tryjob.Status_ENDED, 197 Result: &tryjob.Result{ 198 Status: tryjob.Result_FAILED_TRANSIENTLY, 199 Output: &recipe.Output{ 200 Retry: recipe.Output_OUTPUT_RETRY_DENIED, 201 }, 202 }, 203 }, 204 }, 205 Reason: "tryjob explicitly denies retry in its output", 206 }, 207 }, 208 }, 209 }) 210 }) 211 }) 212 }) 213 }) 214 }