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  }