go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cv/internal/common/lease/lease_test.go (about)

     1  // Copyright 2021 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 lease
    16  
    17  import (
    18  	"context"
    19  	"math/rand"
    20  	"testing"
    21  	"time"
    22  
    23  	"go.chromium.org/luci/common/clock"
    24  	"go.chromium.org/luci/common/data/rand/mathrand"
    25  	"go.chromium.org/luci/common/errors"
    26  	"go.chromium.org/luci/common/retry"
    27  
    28  	"go.chromium.org/luci/cv/internal/cvtesting"
    29  
    30  	. "github.com/smartystreets/goconvey/convey"
    31  	. "go.chromium.org/luci/common/testing/assertions"
    32  )
    33  
    34  func TestLease(t *testing.T) {
    35  	t.Parallel()
    36  
    37  	Convey("Apply", t, func() {
    38  		ct := cvtesting.Test{}
    39  		ctx, cancel := ct.SetUp(t)
    40  		defer cancel()
    41  		ctx = mathrand.Set(ctx, rand.New(rand.NewSource(1)))
    42  
    43  		rid := ResourceID("foo/1")
    44  		now := clock.Now(ctx).UTC().Truncate(time.Second)
    45  		application := Application{
    46  			ResourceID: rid,
    47  			Holder:     "holder",
    48  			Payload:    []byte("Hey!"),
    49  			ExpireTime: now.Add(1 * time.Minute),
    50  		}
    51  		Convey("Works for new lease", func() {
    52  			actual, err := Apply(ctx, application)
    53  			So(err, ShouldBeNil)
    54  			expected := &Lease{
    55  				ResourceID: rid,
    56  				Holder:     "holder",
    57  				Payload:    []byte("Hey!"),
    58  				ExpireTime: now.Add(1 * time.Minute),
    59  				Token:      []byte{82, 253, 252, 7, 33, 130, 101, 79},
    60  			}
    61  			So(actual, ShouldResemble, expected)
    62  			So(mustLoadLease(ctx, rid), ShouldResemble, expected)
    63  
    64  			Convey("Returns AlreadyInLeaseErr if existing lease is still active", func() {
    65  				ct.Clock.Add(30 * time.Second) // lease expires after 1 minute
    66  				_, err := Apply(ctx, application)
    67  				So(err, ShouldErrLike, &AlreadyInLeaseErr{
    68  					ExpireTime: now.Add(1 * time.Minute), // original lease expiry time
    69  					Holder:     "holder",
    70  					ResourceID: rid,
    71  				})
    72  			})
    73  
    74  			Convey("Succeed if existing lease has expired", func() {
    75  				ct.Clock.Add(2 * time.Minute) // lease expires after 1 minute
    76  				now := clock.Now(ctx).UTC().Truncate(time.Second)
    77  				application := Application{
    78  					ResourceID: rid,
    79  					Holder:     "holder2",
    80  					Payload:    []byte("Heyo!"),
    81  					ExpireTime: now.Add(1 * time.Minute),
    82  				}
    83  				actual, err := Apply(ctx, application)
    84  				So(err, ShouldBeNil)
    85  				expected := &Lease{
    86  					ResourceID: rid,
    87  					Holder:     "holder2",
    88  					Payload:    []byte("Heyo!"),
    89  					ExpireTime: now.Add(1 * time.Minute),
    90  					Token:      []byte{22, 63, 95, 15, 154, 98, 29, 114},
    91  				}
    92  				So(actual, ShouldResemble, expected)
    93  				So(mustLoadLease(ctx, rid), ShouldResemble, expected)
    94  			})
    95  		})
    96  
    97  		Convey("Truncates to millisecond", func() {
    98  			application.ExpireTime = now.Add(1 * time.Minute).Add(3141593 * time.Nanosecond) // 3.141593 ms
    99  			actual, err := Apply(ctx, application)
   100  			So(err, ShouldBeNil)
   101  			expected := &Lease{
   102  				ResourceID: rid,
   103  				Holder:     "holder",
   104  				Payload:    []byte("Hey!"),
   105  				ExpireTime: now.Add(1 * time.Minute).Add(3 * time.Millisecond),
   106  				Token:      []byte{82, 253, 252, 7, 33, 130, 101, 79},
   107  			}
   108  			So(actual, ShouldResemble, expected)
   109  		})
   110  	})
   111  
   112  	Convey("Terminate", t, func() {
   113  		ct := cvtesting.Test{}
   114  		ctx, cancel := ct.SetUp(t)
   115  		defer cancel()
   116  
   117  		rid := ResourceID("foo/1")
   118  		now := clock.Now(ctx).UTC().Truncate(time.Second)
   119  		l, err := Apply(ctx, Application{
   120  			ResourceID: rid,
   121  			Holder:     "holder",
   122  			ExpireTime: now.Add(1 * time.Minute),
   123  		})
   124  		So(err, ShouldBeNil)
   125  
   126  		Convey("Works", func() {
   127  			err := l.Terminate(ctx)
   128  			So(err, ShouldBeNil)
   129  			So(mustLoadLease(ctx, rid), ShouldBeNil)
   130  
   131  			Convey("No-op if lease is already terminated", func() {
   132  				err := l.Terminate(ctx)
   133  				So(err, ShouldBeNil)
   134  				So(mustLoadLease(ctx, rid), ShouldBeNil)
   135  			})
   136  		})
   137  
   138  		Convey("Errors if lease is not current", func() {
   139  			ct.Clock.Add(2 * time.Minute)
   140  			now := clock.Now(ctx).UTC().Truncate(time.Second)
   141  			_, err := Apply(ctx, Application{
   142  				ResourceID: rid,
   143  				Holder:     "holder2",
   144  				ExpireTime: now.Add(1 * time.Minute),
   145  			})
   146  			So(err, ShouldBeNil)
   147  			So(l.Terminate(ctx), ShouldResemble, &AlreadyInLeaseErr{
   148  				ExpireTime: now.Add(1 * time.Minute), // original lease expiry time
   149  				Holder:     "holder2",
   150  				ResourceID: rid,
   151  			})
   152  		})
   153  	})
   154  
   155  	Convey("Extend", t, func() {
   156  		ct := cvtesting.Test{}
   157  		ctx, cancel := ct.SetUp(t)
   158  		defer cancel()
   159  		ctx = mathrand.Set(ctx, rand.New(rand.NewSource(1)))
   160  
   161  		rid := ResourceID("foo/1")
   162  		now := clock.Now(ctx).UTC().Truncate(time.Second)
   163  		l, err := Apply(ctx, Application{
   164  			ResourceID: rid,
   165  			Holder:     "holder",
   166  			ExpireTime: now.Add(1 * time.Minute),
   167  			Payload:    []byte("stuff"),
   168  		})
   169  		So(err, ShouldBeNil)
   170  
   171  		Convey("Works", func() {
   172  			err := l.Extend(ctx, 1*time.Minute)
   173  			So(err, ShouldBeNil)
   174  			expected := &Lease{
   175  				ResourceID: rid,
   176  				Holder:     "holder",
   177  				ExpireTime: now.Add(2 * time.Minute),
   178  				Token:      []byte{22, 63, 95, 15, 154, 98, 29, 114},
   179  				Payload:    []byte("stuff"),
   180  			}
   181  			So(l, ShouldResemble, expected)
   182  			So(mustLoadLease(ctx, rid), ShouldResemble, expected)
   183  		})
   184  
   185  		Convey("Truncates to millisecond", func() {
   186  			err := l.Extend(ctx, 3141593*time.Nanosecond) // 3.141593 ms
   187  			So(err, ShouldBeNil)
   188  			expected := &Lease{
   189  				ResourceID: rid,
   190  				Holder:     "holder",
   191  				ExpireTime: now.Add(1 * time.Minute).Add(3 * time.Millisecond),
   192  				Token:      []byte{22, 63, 95, 15, 154, 98, 29, 114},
   193  				Payload:    []byte("stuff"),
   194  			}
   195  			So(l, ShouldResemble, expected)
   196  		})
   197  
   198  		Convey("Errors if lease has expired", func() {
   199  			ct.Clock.Add(2 * time.Minute)
   200  			err := l.Extend(ctx, 1*time.Minute)
   201  			So(err, ShouldErrLike, "can't extend an expired lease")
   202  		})
   203  
   204  		Convey("Errors if lease doesn't exist in Datastore", func() {
   205  			ct.Clock.Add(30 * time.Second)
   206  			So(l.Terminate(ctx), ShouldBeNil)
   207  			err := l.Extend(ctx, 1*time.Minute)
   208  			So(err, ShouldErrLike, "target lease doesn't exist in datastore")
   209  		})
   210  
   211  		Convey("Errors if lease is not current in Datastore", func() {
   212  			ct.Clock.Add(30 * time.Second)
   213  			So(l.Terminate(ctx), ShouldBeNil)
   214  			_, err := Apply(ctx, Application{
   215  				ResourceID: rid,
   216  				Holder:     "holder2",
   217  				ExpireTime: now.UTC().Add(1 * time.Minute),
   218  			})
   219  			So(err, ShouldBeNil)
   220  			err = l.Extend(ctx, 1*time.Minute)
   221  			So(err, ShouldResemble, &AlreadyInLeaseErr{
   222  				ExpireTime: now.Add(1 * time.Minute),
   223  				Holder:     "holder2",
   224  				ResourceID: rid,
   225  			})
   226  		})
   227  	})
   228  }
   229  
   230  func mustLoadLease(ctx context.Context, rid ResourceID) *Lease {
   231  	ret, err := Load(ctx, rid)
   232  	if err != nil {
   233  		panic(err)
   234  	}
   235  	return ret
   236  }
   237  
   238  func TestRetryIfLeased(t *testing.T) {
   239  	t.Parallel()
   240  
   241  	Convey("RetryIfLeased", t, func() {
   242  		ct := cvtesting.Test{}
   243  		ctx, cancel := ct.SetUp(t)
   244  		defer cancel()
   245  
   246  		innerDelay, leaseDur := 2*time.Minute, 3*time.Minute
   247  		innerPolicy := func() retry.Iterator {
   248  			return &retry.Limited{
   249  				Delay:   innerDelay,
   250  				Retries: 1,
   251  			}
   252  		}
   253  		expire := clock.Now(ctx).UTC().Truncate(time.Second).Add(leaseDur)
   254  		leaseErr := &AlreadyInLeaseErr{ExpireTime: expire}
   255  		notLeaseErr := errors.New("successful error")
   256  
   257  		Convey("with nil inner", func() {
   258  			it := RetryIfLeased(nil)()
   259  
   260  			Convey("returns stop, if not AlreadyInLeaseErr", func() {
   261  				So(it.Next(ctx, notLeaseErr), ShouldEqual, retry.Stop)
   262  			})
   263  			Convey("returns the lease expiration, if AlreadyInLeaseErr", func() {
   264  				So(it.Next(ctx, leaseErr).Truncate(time.Second), ShouldEqual, leaseDur)
   265  			})
   266  		})
   267  
   268  		Convey("with limited inner", func() {
   269  			it := RetryIfLeased(innerPolicy)()
   270  
   271  			Convey("returns inner.next(), if not AlreadyInLeaseErr", func() {
   272  				So(it.Next(ctx, notLeaseErr), ShouldEqual, innerDelay)
   273  				So(it.Next(ctx, notLeaseErr), ShouldEqual, retry.Stop)
   274  			})
   275  			Convey("returns whichever comes earlier", func() {
   276  				Convey("inner.Delay < expiration", func() {
   277  					So(it.Next(ctx, leaseErr), ShouldEqual, innerDelay)
   278  				})
   279  				Convey("inner.Delay > expiration", func() {
   280  					it.(*retryIfLeasedIterator).inner.(*retry.Limited).Delay *= 2
   281  					So(it.Next(ctx, leaseErr), ShouldEqual, leaseDur)
   282  				})
   283  				Convey("inner.Delay > 0 but inner returns stop", func() {
   284  					So(it.Next(ctx, notLeaseErr), ShouldEqual, innerDelay)
   285  					So(it.Next(ctx, leaseErr), ShouldEqual, retry.Stop)
   286  				})
   287  			})
   288  		})
   289  	})
   290  }