repositories / bugabinga.net
bugabinga.net
personal infrastructure for bugabinga!
owned by admin
services/luci/internal/lifecycle/lifecycle.go
Rawpackage lifecycle
import (
"encoding/json"
"errors"
"fmt"
"io/fs"
"os"
"path/filepath"
"sort"
"strings"
"syscall"
"time"
"bugabinga.net/luci/internal/history"
)
type Tombstone struct {
RunID string `json:"run_id"`
DeletedAt time.Time `json:"deleted_at"`
}
type Preview struct {
RunID string `json:"run_id"`
Events []history.Event `json:"events"`
Files []File `json:"files"`
Bytes int64 `json:"reclaimable_bytes"`
}
type File struct {
Path string `json:"path"`
Size int64 `json:"size"`
}
type Lock struct{ file *os.File }
// TryLockRun serializes deletion with processing one run.
// shared=false never waits, so an operator receives a definite busy result.
func TryLockRun(dataDir, runID string, shared bool) (*Lock, error) {
if !safe(runID) {
return nil, fmt.Errorf("unsafe run id %q", runID)
}
dir := filepath.Join(dataDir, "locks")
if err := os.MkdirAll(dir, 0o755); err != nil {
return nil, err
}
file, err := os.OpenFile(filepath.Join(dir, runID+".lock"), os.O_CREATE|os.O_RDWR, 0o644)
if err != nil {
return nil, err
}
op := syscall.LOCK_NB
if shared {
op |= syscall.LOCK_SH
} else {
op |= syscall.LOCK_EX
}
if err := syscall.Flock(int(file.Fd()), op); err != nil {
file.Close()
if errors.Is(err, syscall.EWOULDBLOCK) {
return nil, fmt.Errorf("run %s is being processed", runID)
}
return nil, err
}
return &Lock{file: file}, nil
}
func (l *Lock) Close() error {
if l == nil || l.file == nil {
return nil
}
err := syscall.Flock(int(l.file.Fd()), syscall.LOCK_UN)
return errors.Join(err, l.file.Close())
}
func Deleted(dataDir, runID string) (bool, error) {
_, err := os.Stat(path(dataDir, runID))
if errors.Is(err, os.ErrNotExist) {
return false, nil
}
return err == nil, err
}
func Filter(dataDir string, events []history.Event) ([]history.Event, error) {
out := events[:0]
seen := map[string]bool{}
for _, event := range events {
deleted, ok := seen[event.RunID]
if !ok {
var err error
deleted, err = Deleted(dataDir, event.RunID)
if err != nil {
return nil, err
}
seen[event.RunID] = deleted
}
if !deleted {
out = append(out, event)
}
}
return out, nil
}
func PreviewRun(dataDir, runID string, events []history.Event) (Preview, error) {
if !safe(runID) {
return Preview{}, fmt.Errorf("unsafe run id %q", runID)
}
p := Preview{RunID: runID}
for _, e := range events {
if e.RunID == runID {
p.Events = append(p.Events, e)
}
}
if len(p.Events) == 0 {
return p, fmt.Errorf("unknown run %s", runID)
}
paths := map[string]bool{filepath.Join(dataDir, "artifacts", runID): true}
for _, e := range p.Events {
if e.ChildID == "" || e.ChildID == "run" {
continue
}
name := runID + "-" + e.ChildID
if strings.HasPrefix(e.ChildID, "legacy-") {
name = runID + "-" + e.Job
}
paths[filepath.Join(dataDir, "live", name+".log")] = true
paths[filepath.Join(dataDir, "logs", name+".log.zst")] = true
}
for root := range paths {
err := filepath.WalkDir(root, func(name string, entry fs.DirEntry, err error) error {
if errors.Is(err, os.ErrNotExist) {
return nil
}
if err != nil {
return err
}
if entry.Type().IsRegular() {
info, err := entry.Info()
if err != nil {
return err
}
p.Files = append(p.Files, File{Path: name, Size: info.Size()})
p.Bytes += info.Size()
}
return nil
})
if err != nil {
return p, err
}
}
sort.Slice(p.Files, func(i, j int) bool { return p.Files[i].Path < p.Files[j].Path })
return p, nil
}
// Delete writes durable visibility barrier before best-effort retryable cleanup.
func Delete(dataDir string, preview Preview) error {
if !safe(preview.RunID) {
return fmt.Errorf("unsafe run id %q", preview.RunID)
}
if err := os.MkdirAll(filepath.Join(dataDir, "deleted"), 0o755); err != nil {
return err
}
data, err := json.Marshal(Tombstone{RunID: preview.RunID, DeletedAt: time.Now().UTC()})
if err != nil {
return err
}
tmp := path(dataDir, preview.RunID) + ".tmp"
file, err := os.OpenFile(tmp, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o644)
if err != nil {
return err
}
if _, err = file.Write(data); err == nil {
err = file.Sync()
}
if closeErr := file.Close(); err == nil {
err = closeErr
}
if err != nil {
return err
}
if err := os.Rename(tmp, path(dataDir, preview.RunID)); err != nil {
return err
}
dir, err := os.Open(filepath.Join(dataDir, "deleted"))
if err != nil {
return err
}
err = dir.Sync()
closeErr := dir.Close()
if err != nil {
return err
}
if closeErr != nil {
return closeErr
}
for _, file := range preview.Files {
if err := os.Remove(file.Path); err != nil && !errors.Is(err, os.ErrNotExist) {
return err
}
}
return os.RemoveAll(filepath.Join(dataDir, "artifacts", preview.RunID))
}
func path(dataDir, runID string) string { return filepath.Join(dataDir, "deleted", runID+".json") }
func safe(id string) bool { return id != "" && filepath.Base(id) == id && id != "." && id != ".." }