go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/lucictx/deadline_test.go (about) 1 // Copyright 2020 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 lucictx 16 17 import ( 18 "context" 19 "fmt" 20 "os" 21 "sync" 22 "testing" 23 "time" 24 25 . "github.com/smartystreets/goconvey/convey" 26 27 "go.chromium.org/luci/common/clock" 28 "go.chromium.org/luci/common/clock/testclock" 29 "go.chromium.org/luci/common/errors" 30 "go.chromium.org/luci/common/system/signals" 31 . "go.chromium.org/luci/common/testing/assertions" 32 ) 33 34 // shouldWaitForNotDone tests if the context's .Done() channel is still blocked. 35 func shouldWaitForNotDone(actual any, expected ...any) string { 36 if len(expected) > 0 { 37 return fmt.Sprintf("shouldWaitForNotDone requires 0 values, got %d", len(expected)) 38 } 39 40 if actual == nil { 41 return ShouldNotBeNil(actual) 42 } 43 44 ctx, ok := actual.(context.Context) 45 if !ok { 46 return ShouldHaveSameTypeAs(actual, context.Context(nil)) 47 } 48 49 if ctx == nil { 50 return ShouldNotBeNil(actual) 51 } 52 53 select { 54 case <-ctx.Done(): 55 return "Expected context NOT to be Done(), but it was." 56 case <-time.After(100 * time.Millisecond): 57 return "" 58 } 59 } 60 61 var mockSigMu = sync.Mutex{} 62 var mockSigSet = make(map[chan<- os.Signal]struct{}) 63 64 func mockGenerateInterrupt() { 65 mockSigMu.Lock() 66 defer mockSigMu.Unlock() 67 68 if len(mockSigSet) == 0 { 69 panic(errors.New( 70 "mockGenerateInterrupt but no handlers registered; Would have terminated program")) 71 } 72 73 for ch := range mockSigSet { 74 select { 75 case ch <- os.Interrupt: 76 default: 77 } 78 } 79 } 80 81 func assertEmptySignals() { 82 mockSigMu.Lock() 83 defer mockSigMu.Unlock() 84 So(mockSigSet, ShouldBeEmpty) 85 } 86 87 func init() { 88 interrupts := signals.Interrupts() 89 checkSig := func(sig os.Signal) { 90 for _, okSig := range interrupts { 91 if sig == okSig { 92 return 93 } 94 } 95 panic(errors.Reason("unsupported mock signal: %s", sig).Err()) 96 } 97 98 signalNotify = func(ch chan<- os.Signal, sigs ...os.Signal) { 99 for _, sig := range sigs { 100 checkSig(sig) 101 } 102 mockSigMu.Lock() 103 mockSigSet[ch] = struct{}{} 104 mockSigMu.Unlock() 105 } 106 107 signalStop = func(ch chan<- os.Signal) { 108 mockSigMu.Lock() 109 delete(mockSigSet, ch) 110 mockSigMu.Unlock() 111 } 112 } 113 114 func TestDeadline(t *testing.T) { 115 // not Parallel because this uses the global mock signalNotify. 116 // t.Parallel() 117 118 Convey(`TrackSoftDeadline`, t, func() { 119 t0 := testclock.TestTimeUTC 120 ctx, tc := testclock.UseTime(context.Background(), t0) 121 ctx, cancel := context.WithCancel(ctx) 122 defer cancel() 123 defer assertEmptySignals() 124 125 // we explicitly remove the section to make these tests work correctly when 126 // run in a context using LUCI_CONTEXT. 127 ctx = Set(ctx, "deadline", nil) 128 129 Convey(`Empty context`, func() { 130 ac, shutdown := TrackSoftDeadline(ctx, 5*time.Second) 131 defer shutdown() 132 133 deadline, ok := ac.Deadline() 134 So(ok, ShouldBeFalse) 135 So(deadline.IsZero(), ShouldBeTrue) 136 137 // however, Interrupt/SIGTERM handler is still installed 138 mockGenerateInterrupt() 139 140 // soft deadline will happen, but context.Done won't. 141 So(<-SoftDeadlineDone(ac), ShouldEqual, InterruptEvent) 142 So(ac, shouldWaitForNotDone) 143 144 // Advance the clock by 25s, and presto 145 tc.Add(25 * time.Second) 146 <-ac.Done() 147 }) 148 149 Convey(`deadline context`, func() { 150 ctx, cancel := clock.WithDeadline(ctx, t0.Add(100*time.Second)) 151 defer cancel() 152 153 ac, shutdown := TrackSoftDeadline(ctx, 5*time.Second) 154 defer shutdown() 155 156 hardDeadline, ok := ac.Deadline() 157 So(ok, ShouldBeTrue) 158 // hard deadline is still 95s because we the presumed grace period for the 159 // context was 30s, but we reserved 5s for cleanup. Thus, this should end 160 // 5s before the overall deadline, 161 So(hardDeadline, ShouldEqual, t0.Add(95*time.Second)) 162 got := GetDeadline(ac) 163 164 expect := &Deadline{GracePeriod: 25} 165 // SoftDeadline is always GracePeriod earlier than the hard (context) 166 // deadline. 167 expect.SetSoftDeadline(t0.Add(70 * time.Second)) 168 So(got, ShouldResembleProto, expect) 169 shutdown() 170 <-SoftDeadlineDone(ac) // force monitor to make timer before we increment the clock 171 tc.Add(25 * time.Second) 172 <-ac.Done() 173 }) 174 175 Convey(`deadline context reserve`, func() { 176 ctx, cancel := clock.WithDeadline(ctx, t0.Add(95*time.Second)) 177 defer cancel() 178 179 ac, shutdown := TrackSoftDeadline(ctx, 0) 180 defer shutdown() 181 182 deadline, ok := ac.Deadline() 183 So(ok, ShouldBeTrue) 184 // hard deadline is 95s because we reserved 5s. 185 So(deadline, ShouldEqual, t0.Add(95*time.Second)) 186 got := GetDeadline(ac) 187 188 expect := &Deadline{GracePeriod: 30} 189 // SoftDeadline is always GracePeriod earlier than the hard (context) 190 // deadline. 191 expect.SetSoftDeadline(t0.Add(65 * time.Second)) 192 So(got, ShouldResembleProto, expect) 193 shutdown() 194 <-SoftDeadlineDone(ac) // force monitor to make timer before we increment the clock 195 tc.Add(30 * time.Second) 196 <-ac.Done() 197 }) 198 199 Convey(`Deadline in LUCI_CONTEXT`, func() { 200 externalSoftDeadline := t0.Add(100 * time.Second) 201 202 // Note, LUCI_CONTEXT asserts that non-zero SoftDeadlines must be enforced 203 // by 'an external process', so we mock that with the goroutine here. 204 // 205 // Must do clock.After outside goroutine to force this time calculation to 206 // happen before we start manipulating `tc`. 207 externalTimeout := clock.After(ctx, 100*time.Second) 208 go func() { 209 if (<-externalTimeout).Err == nil { 210 mockGenerateInterrupt() 211 } 212 }() 213 214 dl := &Deadline{GracePeriod: 40} 215 dl.SetSoftDeadline(externalSoftDeadline) // 100s into the future 216 217 ctx := SetDeadline(ctx, dl) 218 219 Convey(`no deadline in context`, func() { 220 ac, shutdown := TrackSoftDeadline(ctx, 5*time.Second) 221 defer shutdown() 222 223 softDeadline := GetDeadline(ac).SoftDeadlineTime() 224 So(softDeadline, ShouldHappenWithin, time.Millisecond, externalSoftDeadline) 225 226 hardDeadline, ok := ac.Deadline() 227 So(ok, ShouldBeTrue) 228 // hard deadline is soft deadline + adjusted grace period. 229 // Cleanup reservation of 5s means that the adjusted grace period is 230 // 35s. 231 So(hardDeadline, ShouldHappenWithin, time.Millisecond, externalSoftDeadline.Add(35*time.Second)) 232 233 Convey(`natural expiration`, func() { 234 tc.Add(100 * time.Second) 235 So(<-SoftDeadlineDone(ac), ShouldEqual, TimeoutEvent) 236 So(ac, shouldWaitForNotDone) 237 238 tc.Add(35 * time.Second) 239 <-ac.Done() 240 241 // We should have ended right around the deadline; there's some slop 242 // in the clock package though, and this doesn't seem to be zero. 243 So(tc.Now(), ShouldHappenWithin, time.Millisecond, hardDeadline) 244 }) 245 246 Convey(`signal`, func() { 247 mockGenerateInterrupt() 248 So(<-SoftDeadlineDone(ac), ShouldEqual, InterruptEvent) 249 250 So(ac, shouldWaitForNotDone) 251 252 tc.Add(35 * time.Second) 253 <-ac.Done() 254 255 // should still have 65s before the soft deadline 256 So(tc.Now(), ShouldHappenWithin, time.Millisecond, softDeadline.Add(-65*time.Second)) 257 }) 258 259 Convey(`cancel context`, func() { 260 cancel() 261 So(<-SoftDeadlineDone(ac), ShouldEqual, ClosureEvent) 262 <-ac.Done() 263 }) 264 }) 265 266 Convey(`earlier deadline in context`, func() { 267 ctx, cancel := clock.WithDeadline(ctx, externalSoftDeadline.Add(-50*time.Second)) 268 defer cancel() 269 270 ac, shutdown := TrackSoftDeadline(ctx, 5*time.Second) 271 defer shutdown() 272 273 hardDeadline, ok := ac.Deadline() 274 So(ok, ShouldBeTrue) 275 So(hardDeadline, ShouldEqual, externalSoftDeadline.Add(-55*time.Second)) 276 277 Convey(`natural expiration`, func() { 278 tc.Add(10 * time.Second) 279 So(<-SoftDeadlineDone(ac), ShouldEqual, TimeoutEvent) 280 So(ac, shouldWaitForNotDone) 281 282 tc.Add(35 * time.Second) 283 <-ac.Done() 284 285 // We should have ended right around the deadline; there's some slop 286 // in the clock package though, and this doesn't seem to be zero. 287 So(tc.Now(), ShouldHappenWithin, time.Millisecond, hardDeadline) 288 }) 289 290 Convey(`signal`, func() { 291 mockGenerateInterrupt() 292 So(<-SoftDeadlineDone(ac), ShouldEqual, InterruptEvent) 293 294 So(ac, shouldWaitForNotDone) 295 296 tc.Add(35 * time.Second) 297 <-ac.Done() 298 299 // Should have about 10s of time left before the deadline. 300 So(tc.Now(), ShouldHappenWithin, time.Millisecond, hardDeadline.Add(-10*time.Second)) 301 }) 302 }) 303 304 }) 305 }) 306 }