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  }