go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cipkg/base/workflow/packages.go (about) 1 // Copyright 2023 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 workflow 16 17 import ( 18 "context" 19 "errors" 20 "fmt" 21 "io/fs" 22 "os" 23 "path/filepath" 24 "time" 25 26 "go.chromium.org/luci/cipkg/base/actions" 27 "go.chromium.org/luci/cipkg/core" 28 "go.chromium.org/luci/common/logging" 29 "go.chromium.org/luci/common/system/filesystem" 30 31 "github.com/danjacques/gofslock/fslock" 32 ) 33 34 // blocker is used by fslock for waiting lock. 35 func blocker() error { 36 time.Sleep(time.Millisecond * 10) 37 return nil 38 } 39 40 // LocalPackageManager implements core.PackageManager. 41 var _ core.PackageManager = &LocalPackageManager{} 42 43 // LocalPackageManager is a PackageManager implementation that stores packages 44 // locally. It supports recording package references acrossing multiple 45 // instances using fslock. 46 type LocalPackageManager struct { 47 storagePath string 48 } 49 50 func NewLocalPackageManager(path string) (*LocalPackageManager, error) { 51 if err := os.MkdirAll(path, fs.ModePerm); err != nil { 52 return nil, fmt.Errorf("initialize local storage failed: %s: %w", path, err) 53 } 54 s := &LocalPackageManager{ 55 storagePath: path, 56 } 57 return s, nil 58 } 59 60 // Get returns the handler for the package. 61 func (pm *LocalPackageManager) Get(id string) core.PackageHandler { 62 return &localPackageHandler{ 63 baseDirectory: filepath.Join(pm.storagePath, id), 64 lockFile: filepath.Join(pm.storagePath, fmt.Sprintf(".%s.lock", id)), 65 } 66 } 67 68 // Prune try remove packages if: 69 // - Not available, or 70 // - Haven't been used for `ttl` time. 71 func (pm *LocalPackageManager) Prune(c context.Context, ttl time.Duration, max int) { 72 deadline := time.Now().Add(-ttl) 73 locks, err := fs.Glob(os.DirFS(pm.storagePath), ".*.lock") 74 if err != nil { 75 logging.WithError(err).Warningf(c, "failed to list locks") 76 } 77 pruned := 0 78 for _, l := range locks { 79 id := l[1 : len(l)-5] // remove prefix "." and suffix ".lock" 80 pkg := pm.Get(id).(*localPackageHandler) 81 if t := pkg.lastUsed(); ttl == 0 || t.Before(deadline) { 82 if removed, err := pkg.TryRemove(); err != nil { 83 logging.WithError(err).Warningf(c, "failed to remove package") 84 } else if removed { 85 logging.Debugf(c, "prune: remove package (not used since %s): %s", t, id) 86 if pruned++; pruned == max { 87 logging.Debugf(c, "prune: hit prune limit of %d ", max) 88 break 89 } 90 } 91 } else { 92 logging.Debugf(c, "prune: skip package (not used since %s): %s", t, id) 93 } 94 } 95 } 96 97 type localPackageHandler struct { 98 baseDirectory string 99 lockFile string 100 rlockHandle fslock.Handle 101 } 102 103 func (h *localPackageHandler) OutputDirectory() string { 104 return filepath.Join(h.baseDirectory, "contents") 105 } 106 107 func (h *localPackageHandler) LoggingDirectory() string { 108 return filepath.Join(h.baseDirectory, "logs") 109 } 110 111 func (h *localPackageHandler) Build(builder func() error) error { 112 if h.rlockHandle != nil { 113 return fmt.Errorf("can't build package when read lock is held") 114 } 115 116 return fslock.WithBlocking(h.lockFile, blocker, func() error { 117 if t := h.lastUsed(); !t.IsZero() { 118 return nil 119 } 120 121 if err := filesystem.RemoveAll(h.baseDirectory); err != nil { 122 return fmt.Errorf("failed to remove package dir: %s: %w", h.baseDirectory, err) 123 } 124 if err := os.MkdirAll(h.OutputDirectory(), fs.ModePerm); err != nil { 125 return fmt.Errorf("failed to create package dir: %s: %w", h.OutputDirectory(), err) 126 } 127 128 if err := builder(); err != nil { 129 return fmt.Errorf("failed to execute builder: %w", err) 130 } 131 132 f, err := os.Create(h.stampPath()) 133 if err != nil { 134 return fmt.Errorf("failed to create stamp file: %s: %w", h.stampPath(), err) 135 } 136 defer f.Close() 137 if err := h.touch(); err != nil { 138 return fmt.Errorf("failed to touch stamp file: %s: %w", h.stampPath(), err) 139 } 140 return nil 141 }) 142 } 143 144 func (h *localPackageHandler) TryRemove() (ok bool, err error) { 145 switch err := fslock.With(h.lockFile, func() error { 146 if err := filesystem.RemoveAll(h.baseDirectory); err != nil { 147 return fmt.Errorf("failed to remove package dir: %s: %w", h.baseDirectory, err) 148 } 149 return nil 150 }); err { 151 case nil: 152 if err := filesystem.RemoveAll(h.lockFile); err != nil { 153 return false, nil 154 } 155 return true, nil 156 case fslock.ErrLockHeld: 157 return false, nil 158 default: 159 return false, err 160 } 161 } 162 163 func (h *localPackageHandler) lastUsed() time.Time { 164 if s, err := os.Stat(h.stampPath()); err == nil { 165 return s.ModTime() 166 } 167 168 return time.Time{} 169 } 170 171 func (h *localPackageHandler) IncRef() error { 172 if h.rlockHandle != nil { 173 return fmt.Errorf("acquire read lock multiple times on same package") 174 } 175 176 rl, err := fslock.LockSharedBlocking(h.lockFile, blocker) 177 if err != nil { 178 return fmt.Errorf("failed to acquire read lock: %w", err) 179 } 180 if err := func() error { 181 if err := rl.PreserveExec(); err != nil { 182 return fmt.Errorf("failed to perserve lock: %w", err) 183 } 184 if t := h.lastUsed(); t.IsZero() { 185 return core.ErrPackageNotExist 186 } 187 188 // Update mtime of the stamp since at this point we ensured: 189 // 1. Package is locked and won't be removed 190 // 2. Stamp is presented in the package 191 if err := h.touch(); err != nil { 192 return fmt.Errorf("failed to touch the the stamp: %w", err) 193 } 194 return nil 195 }(); err != nil { 196 return errors.Join(err, rl.Unlock()) 197 } 198 h.rlockHandle = rl 199 return nil 200 } 201 202 func (h *localPackageHandler) DecRef() error { 203 if err := h.rlockHandle.Unlock(); err != nil { 204 return fmt.Errorf("failed to release read lock: %w", err) 205 } 206 h.rlockHandle = nil 207 return nil 208 } 209 210 func (h *localPackageHandler) stampPath() string { 211 return filepath.Join(h.baseDirectory, "stamp") 212 } 213 214 func (h *localPackageHandler) touch() error { 215 return filesystem.Touch(h.stampPath(), time.Time{}, 0644) 216 } 217 218 // MustIncRefRecursiveRuntime will IncRef the package with all its runtime 219 // dependencies recursively. If an error happened, it may panic with only 220 // part of the packages are referenced. 221 func MustIncRefRecursiveRuntime(pkg actions.Package) { 222 if err := doPackageRecursiveRuntime(pkg, make(map[string]struct{}), 223 func(pkg actions.Package) error { return pkg.Handler.IncRef() }); err != nil { 224 panic(err) 225 } 226 } 227 228 // MustDecRefRecursiveRuntime will DecRef the package with all its runtime 229 // dependencies recursively. If an error happened, it may panic with only 230 // part of the packages are dereferenced. 231 func MustDecRefRecursiveRuntime(pkg actions.Package) { 232 if err := doPackageRecursiveRuntime(pkg, make(map[string]struct{}), 233 func(pkg actions.Package) error { return pkg.Handler.DecRef() }); err != nil { 234 panic(err) 235 } 236 } 237 238 func doPackageRecursiveRuntime(pkg actions.Package, visited map[string]struct{}, f func(pkg actions.Package) error) error { 239 if _, ok := visited[pkg.DerivationID]; ok { 240 return nil 241 } 242 if err := f(pkg); err != nil { 243 return err 244 } 245 visited[pkg.DerivationID] = struct{}{} 246 247 for _, d := range pkg.RuntimeDependencies { 248 if err := doPackageRecursiveRuntime(d, visited, f); err != nil { 249 return err 250 } 251 } 252 return nil 253 }