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  }