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 }