go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/mmutex/lib/lock_file_helpers.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  	"fmt"
    20  	"os"
    21  	"path/filepath"
    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  
    31  // LockFileEnvVariable specifies the directory of the lock file.
    32  const LockFileEnvVariable = "MMUTEX_LOCK_DIR"
    33  
    34  // LockFileName specifies the name of the lock file within $MMUTEX_LOCK_DIR.
    35  const LockFileName = "mmutex.lock"
    36  
    37  // DrainFileName specifies the name of the drain file within $MMUTEX_LOCK_DIR.
    38  const DrainFileName = "mmutex.drain"
    39  
    40  // DefaultCommandTimeout is the total amount of time, including lock acquisition
    41  // and command runtime, allotted to running a command through mmutex.
    42  const DefaultCommandTimeout = 2 * time.Hour
    43  
    44  // lockAcquisitionAttempts is the number of times that the blocker should attempt to
    45  // acquire the lock. This is approximate, especially when making a high number of
    46  // attempts over a short period of time.
    47  const lockAcquisitionAttempts = 100
    48  
    49  // defaultLockPollingInterval is the polling interval used if no command
    50  // timeout is specified.
    51  const defaultLockPollingInterval = time.Millisecond
    52  
    53  // computeMutexPaths returns the lock and drain file paths based on the environment,
    54  // or empty strings if no lock files should be used.
    55  func computeMutexPaths(env subcommands.Env) (lockFilePath string, drainFilePath string, err error) {
    56  	envVar := env[LockFileEnvVariable]
    57  	if !envVar.Exists {
    58  		return "", "", nil
    59  	}
    60  
    61  	lockFileDir := envVar.Value
    62  	if !filepath.IsAbs(lockFileDir) {
    63  		return "", "", errors.Reason("Lock file directory %s must be an absolute path", lockFileDir).Err()
    64  	}
    65  
    66  	if _, err := os.Stat(lockFileDir); os.IsNotExist(err) {
    67  		fmt.Printf("Lock file directory %s does not exist, mmutex acting as a passthrough.\n", lockFileDir)
    68  		return "", "", nil
    69  	}
    70  
    71  	return filepath.Join(lockFileDir, LockFileName), filepath.Join(lockFileDir, DrainFileName), nil
    72  }
    73  
    74  func createLockBlocker(ctx context.Context) fslock.Blocker {
    75  	pollingInterval := defaultLockPollingInterval
    76  	if deadline, ok := ctx.Deadline(); ok {
    77  		pollingInterval = clock.Until(ctx, deadline) / (lockAcquisitionAttempts - 1)
    78  	}
    79  
    80  	// crbug.com/1038136
    81  	// The default timeout is 120 minutes and the lock acquisition attempts is 100.
    82  	// With this setting, the polling interval is 72 seconds which is too long for
    83  	// lock acquisition. Setting max interval to be 1 second.
    84  	if pollingInterval > time.Second {
    85  		pollingInterval = time.Second
    86  	}
    87  
    88  	return func() error {
    89  		if clock.Sleep(ctx, pollingInterval).Err != nil {
    90  			return fslock.ErrLockHeld
    91  		}
    92  
    93  		// Returning nil signals that the lock should be retried.
    94  		return nil
    95  	}
    96  }
    97  
    98  // blockWhileFileExists blocks until the file located at path no longer exists.
    99  // For convenience, this method reuses the Blocker interface exposed by fslock
   100  // and used elsewhere in this package.
   101  func blockWhileFileExists(path string, blocker fslock.Blocker) error {
   102  	for {
   103  		if _, err := os.Stat(path); os.IsNotExist(err) {
   104  			break
   105  		} else if err != nil {
   106  			return errors.Annotate(err, "failed to stat %s", path).Err()
   107  		}
   108  
   109  		if err := blocker(); err != nil {
   110  			return errors.New("timed out waiting for drain file to disappear")
   111  		}
   112  	}
   113  
   114  	return nil
   115  }