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  }