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  }