go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/mmutex/lib/shared_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 TestShared(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("RunShared", 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(RunShared(ctx, env, fnThatReturns(errors.Reason("test error").Err())), ShouldErrLike, "test error") 57 }) 58 59 Convey("times out if an 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(RunShared(ctx, env, fnThatReturns(nil)), ShouldErrLike, "fslock: lock is held") 67 So(ctx.Err(), ShouldErrLike, context.DeadlineExceeded) 68 }) 69 70 Convey("uses context parameter as basis for new context", func() { 71 ctx, cancel := context.WithCancel(ctx) 72 cancel() 73 err := RunShared(ctx, env, func(ctx context.Context) error { 74 return clock.Sleep(ctx, time.Millisecond).Err 75 }) 76 So(err, ShouldErrLike, context.Canceled) 77 }) 78 79 Convey("executes the command if shared lock already held", func() { 80 handle, err := fslock.LockShared(lockFilePath) 81 So(err, ShouldBeNil) 82 defer handle.Unlock() 83 84 So(RunShared(ctx, env, fnThatReturns(nil)), ShouldBeNil) 85 }) 86 87 Convey("waits for drain file to go away before requesting lock", func() { 88 file, err := os.OpenFile(drainFilePath, os.O_RDONLY|os.O_CREATE, 0666) 89 So(err, ShouldBeNil) 90 err = file.Close() 91 So(err, ShouldBeNil) 92 93 commandResult := make(chan error) 94 runSharedErr := make(chan error) 95 go func() { 96 runSharedErr <- RunShared(ctx, env, func(ctx context.Context) error { 97 // Block RunShared() immediately after it starts executing the command. 98 return <-commandResult 99 }) 100 }() 101 102 // Sleep a millisecond so that RunShared() has an opportunity to reach the 103 // logic where it checks for a drain file. 104 clock.Sleep(ctx, time.Millisecond) 105 106 // The lock should be available: RunShared() didn't acquire it due to the presence 107 // of the drain file. Verify this by acquiring and immediately releasing the lock. 108 handle, err := fslock.Lock(lockFilePath) 109 So(err, ShouldBeNil) 110 err = handle.Unlock() 111 So(err, ShouldBeNil) 112 113 // Removing the drain file should allow RunShared() to progress as normal. 114 err = os.Remove(drainFilePath) 115 So(err, ShouldBeNil) 116 117 commandResult <- nil 118 So(<-runSharedErr, ShouldBeNil) 119 }) 120 121 Convey("times out if drain file doesn't go away", func() { 122 file, err := os.OpenFile(drainFilePath, os.O_RDONLY|os.O_CREATE, 0666) 123 So(err, ShouldBeNil) 124 err = file.Close() 125 So(err, ShouldBeNil) 126 defer os.Remove(drainFilePath) 127 128 ctx, cancel := context.WithTimeout(ctx, 5*time.Millisecond) 129 defer cancel() 130 runSharedErr := make(chan error) 131 go func() { 132 runSharedErr <- RunShared(ctx, env, fnThatReturns(nil)) 133 }() 134 135 So(<-runSharedErr, ShouldErrLike, "timed out waiting for drain file to disappear") 136 }) 137 138 Convey("acts as a passthrough if lockFileDir is empty", func() { 139 So(RunShared(ctx, subcommands.Env{}, fnThatReturns(nil)), ShouldBeNil) 140 }) 141 }) 142 }