go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/mmutex/lib/exclusive_test.go (about) 1 // Copyright 2017 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 lib 16 17 import ( 18 "context" 19 "io/ioutil" 20 "os" 21 "testing" 22 "time" 23 24 "github.com/danjacques/gofslock/fslock" 25 "github.com/maruel/subcommands" 26 27 "go.chromium.org/luci/common/clock" 28 "go.chromium.org/luci/common/errors" 29 30 . "github.com/smartystreets/goconvey/convey" 31 . "go.chromium.org/luci/common/testing/assertions" 32 ) 33 34 func TestExclusive(t *testing.T) { 35 var fnThatReturns = func(err error) func(context.Context) error { 36 return func(context.Context) error { 37 return err 38 } 39 } 40 41 Convey("RunExclusive", t, func() { 42 lockFileDir, err := ioutil.TempDir("", "") 43 So(err, ShouldBeNil) 44 defer os.Remove(lockFileDir) 45 env := subcommands.Env{ 46 LockFileEnvVariable: subcommands.EnvVar{ 47 Value: lockFileDir, 48 Exists: true, 49 }, 50 } 51 lockFilePath, drainFilePath, err := computeMutexPaths(env) 52 So(err, ShouldBeNil) 53 ctx := context.Background() 54 55 Convey("returns error from the command", func() { 56 So(RunExclusive(ctx, env, fnThatReturns(errors.Reason("test error").Err())), ShouldErrLike, "test error") 57 }) 58 59 Convey("times out if exclusive lock isn't released", func() { 60 handle, err := fslock.Lock(lockFilePath) 61 So(err, ShouldBeNil) 62 defer handle.Unlock() 63 64 ctx, cancel := context.WithTimeout(ctx, time.Millisecond) 65 defer cancel() 66 So(RunExclusive(ctx, env, fnThatReturns(nil)), ShouldErrLike, "fslock: lock is held") 67 }) 68 69 Convey("times out if shared lock isn't released", func() { 70 handle, err := fslock.LockShared(lockFilePath) 71 So(err, ShouldBeNil) 72 defer handle.Unlock() 73 74 ctx, cancel := context.WithTimeout(ctx, time.Millisecond) 75 defer cancel() 76 So(RunExclusive(ctx, env, fnThatReturns(nil)), ShouldErrLike, "fslock: lock is held") 77 }) 78 79 Convey("uses context parameter as basis for new context", func() { 80 ctx, cancel := context.WithCancel(ctx) 81 cancel() 82 err := RunExclusive(ctx, env, func(ctx context.Context) error { 83 return clock.Sleep(ctx, time.Millisecond).Err 84 }) 85 So(err, ShouldErrLike, context.Canceled) 86 }) 87 88 Convey("respects timeout", func() { 89 handle, err := fslock.Lock(lockFilePath) 90 So(err, ShouldBeNil) 91 defer handle.Unlock() 92 93 ctx, cancel := context.WithTimeout(ctx, time.Millisecond) 94 defer cancel() 95 RunExclusive(ctx, env, fnThatReturns(nil)) 96 So(ctx.Err(), ShouldErrLike, context.DeadlineExceeded) 97 }) 98 99 Convey("creates drain file while acquiring the lock", func() { 100 handle, err := fslock.LockShared(lockFilePath) 101 So(err, ShouldBeNil) 102 defer handle.Unlock() 103 104 go func() { 105 RunExclusive(ctx, env, fnThatReturns(nil)) 106 }() 107 108 // Sleep for a millisecond to allow time for the drain file to be created. 109 clock.Sleep(ctx, 3*time.Millisecond) 110 111 _, err = os.Stat(drainFilePath) 112 So(err, ShouldBeNil) 113 }) 114 115 Convey("removes drain file immediately after acquiring the lock", func() { 116 commandStarted := make(chan struct{}) 117 commandResult := make(chan error) 118 runExclusiveErr := make(chan error) 119 go func() { 120 runExclusiveErr <- RunExclusive(ctx, env, func(ctx context.Context) error { 121 close(commandStarted) 122 123 // Block RunExclusive() immediately after it starts executing the command. 124 // The drain file should have been cleaned up immediately before this point. 125 return <-commandResult 126 }) 127 }() 128 129 // At the time when the command starts but has not yet finished executing 130 // (because it's blocked on the commandResult channel), the drain file 131 // should be removed. 132 <-commandStarted 133 _, err = os.Stat(drainFilePath) 134 So(os.IsNotExist(err), ShouldBeTrue) 135 136 commandResult <- nil 137 So(<-runExclusiveErr, ShouldBeNil) 138 }) 139 140 Convey("acts as a passthrough if lockFileDir is empty", func() { 141 So(RunExclusive(ctx, subcommands.Env{}, fnThatReturns(nil)), ShouldBeNil) 142 }) 143 }) 144 }