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  }