Skip to content

Commit 811f883

Browse files
brianmcgeeBrian McGee
authored and
Brian McGee
committed
feat/bust-cache-validators-change (#14)
Tracks the mod time and size of a formatter's executable in bolt. The cache is busted using the following criteria: - a new formatter has been configured. - an existing formatter has changed (mod time or size) - an existing formatter has been removed from config Also implemented better resolution of symlinks when determining a formatters executable path. Reviewed-on: https://git.numtide.com/numtide/treefmt/pulls/14 Reviewed-by: Jonas Chevalier <[email protected]> Co-authored-by: Brian McGee <[email protected]> Co-committed-by: Brian McGee <[email protected]>
1 parent ada9a72 commit 811f883

File tree

5 files changed

+256
-39
lines changed

5 files changed

+256
-39
lines changed

internal/cache/cache.go

+96-30
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,23 @@ import (
44
"context"
55
"crypto/sha1"
66
"encoding/hex"
7-
"errors"
87
"fmt"
98
"io/fs"
109
"os"
1110
"path/filepath"
1211
"time"
1312

13+
"git.numtide.com/numtide/treefmt/internal/format"
14+
"github.com/charmbracelet/log"
15+
1416
"github.com/adrg/xdg"
1517
"github.com/vmihailenco/msgpack/v5"
1618
bolt "go.etcd.io/bbolt"
1719
)
1820

1921
const (
20-
modifiedBucket = "modified"
22+
pathsBucket = "paths"
23+
formattersBucket = "formatters"
2124
)
2225

2326
// Entry represents a cache entry, indicating the last size and modified time for a file path.
@@ -33,7 +36,9 @@ var db *bolt.DB
3336
//
3437
// The database will be located in `XDG_CACHE_DIR/treefmt/eval-cache/<id>.db`, where <id> is determined by hashing
3538
// the treeRoot path. This associates a given treeRoot with a given instance of the cache.
36-
func Open(treeRoot string, clean bool) (err error) {
39+
func Open(treeRoot string, clean bool, formatters map[string]*format.Formatter) (err error) {
40+
l := log.WithPrefix("cache")
41+
3742
// determine a unique and consistent db name for the tree root
3843
h := sha1.New()
3944
h.Write([]byte(treeRoot))
@@ -45,27 +50,84 @@ func Open(treeRoot string, clean bool) (err error) {
4550
return fmt.Errorf("%w: could not resolve local path for the cache", err)
4651
}
4752

48-
// force a clean of the cache if specified
49-
if clean {
50-
err := os.Remove(path)
51-
if errors.Is(err, os.ErrNotExist) {
52-
err = nil
53-
} else if err != nil {
54-
return fmt.Errorf("%w: failed to clear cache", err)
55-
}
56-
}
57-
5853
db, err = bolt.Open(path, 0o600, nil)
5954
if err != nil {
6055
return fmt.Errorf("%w: failed to open cache", err)
6156
}
6257

6358
err = db.Update(func(tx *bolt.Tx) error {
64-
_, err := tx.CreateBucket([]byte(modifiedBucket))
65-
if errors.Is(err, bolt.ErrBucketExists) {
59+
// create bucket for tracking paths
60+
pathsBucket, err := tx.CreateBucketIfNotExists([]byte(pathsBucket))
61+
if err != nil {
62+
return fmt.Errorf("%w: failed to create paths bucket", err)
63+
}
64+
65+
// create bucket for tracking formatters
66+
formattersBucket, err := tx.CreateBucketIfNotExists([]byte(formattersBucket))
67+
if err != nil {
68+
return fmt.Errorf("%w: failed to create formatters bucket", err)
69+
}
70+
71+
// check for any newly configured or modified formatters
72+
for name, formatter := range formatters {
73+
74+
stat, err := os.Lstat(formatter.Executable())
75+
if err != nil {
76+
return fmt.Errorf("%w: failed to state formatter executable", err)
77+
}
78+
79+
entry, err := getEntry(formattersBucket, name)
80+
if err != nil {
81+
return fmt.Errorf("%w: failed to retrieve entry for formatter", err)
82+
}
83+
84+
clean = clean || entry == nil || !(entry.Size == stat.Size() && entry.Modified == stat.ModTime())
85+
l.Debug(
86+
"checking if formatter has changed",
87+
"name", name,
88+
"clean", clean,
89+
"entry", entry,
90+
"stat", stat,
91+
)
92+
93+
// record formatters info
94+
entry = &Entry{
95+
Size: stat.Size(),
96+
Modified: stat.ModTime(),
97+
}
98+
99+
if err = putEntry(formattersBucket, name, entry); err != nil {
100+
return fmt.Errorf("%w: failed to write formatter entry", err)
101+
}
102+
}
103+
104+
// check for any removed formatters
105+
if err = formattersBucket.ForEach(func(key []byte, _ []byte) error {
106+
_, ok := formatters[string(key)]
107+
if !ok {
108+
// remove the formatter entry from the cache
109+
if err = formattersBucket.Delete(key); err != nil {
110+
return fmt.Errorf("%w: failed to remove formatter entry", err)
111+
}
112+
// indicate a clean is required
113+
clean = true
114+
}
66115
return nil
116+
}); err != nil {
117+
return fmt.Errorf("%w: failed to check for removed formatters", err)
118+
}
119+
120+
if clean {
121+
// remove all path entries
122+
c := pathsBucket.Cursor()
123+
for k, v := c.First(); !(k == nil && v == nil); k, v = c.Next() {
124+
if err = c.Delete(); err != nil {
125+
return fmt.Errorf("%w: failed to remove path entry", err)
126+
}
127+
}
67128
}
68-
return err
129+
130+
return nil
69131
})
70132

71133
return
@@ -93,11 +155,24 @@ func getEntry(bucket *bolt.Bucket, path string) (*Entry, error) {
93155
}
94156
}
95157

158+
// putEntry is a helper for writing cache entries into bolt.
159+
func putEntry(bucket *bolt.Bucket, path string, entry *Entry) error {
160+
bytes, err := msgpack.Marshal(entry)
161+
if err != nil {
162+
return fmt.Errorf("%w: failed to marshal cache entry", err)
163+
}
164+
165+
if err = bucket.Put([]byte(path), bytes); err != nil {
166+
return fmt.Errorf("%w: failed to put cache entry", err)
167+
}
168+
return nil
169+
}
170+
96171
// ChangeSet is used to walk a filesystem, starting at root, and outputting any new or changed paths using pathsCh.
97172
// It determines if a path is new or has changed by comparing against cache entries.
98173
func ChangeSet(ctx context.Context, root string, pathsCh chan<- string) error {
99174
return db.Update(func(tx *bolt.Tx) error {
100-
bucket := tx.Bucket([]byte(modifiedBucket))
175+
bucket := tx.Bucket([]byte(pathsBucket))
101176

102177
return filepath.Walk(root, func(path string, info fs.FileInfo, err error) error {
103178
if err != nil {
@@ -142,13 +217,9 @@ func Update(paths []string) (int, error) {
142217
var changes int
143218

144219
return changes, db.Update(func(tx *bolt.Tx) error {
145-
bucket := tx.Bucket([]byte(modifiedBucket))
220+
bucket := tx.Bucket([]byte(pathsBucket))
146221

147222
for _, path := range paths {
148-
if path == "" {
149-
continue
150-
}
151-
152223
cached, err := getEntry(bucket, path)
153224
if err != nil {
154225
return err
@@ -166,18 +237,13 @@ func Update(paths []string) (int, error) {
166237
continue
167238
}
168239

169-
cacheInfo := Entry{
240+
entry := Entry{
170241
Size: pathInfo.Size(),
171242
Modified: pathInfo.ModTime(),
172243
}
173244

174-
bytes, err := msgpack.Marshal(cacheInfo)
175-
if err != nil {
176-
return fmt.Errorf("%w: failed to marshal mod time", err)
177-
}
178-
179-
if err = bucket.Put([]byte(path), bytes); err != nil {
180-
return fmt.Errorf("%w: failed to put mode time", err)
245+
if err = putEntry(bucket, path, &entry); err != nil {
246+
return err
181247
}
182248
}
183249

internal/cli/format.go

+6-2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package cli
22

33
import (
44
"context"
5+
"errors"
56
"fmt"
67
"os"
78
"os/signal"
@@ -71,7 +72,7 @@ func (f *Format) Run() error {
7172
}
7273

7374
err = formatter.Init(name, globalExcludes)
74-
if err == format.ErrFormatterNotFound && Cli.AllowMissingFormatter {
75+
if errors.Is(err, format.ErrFormatterNotFound) && Cli.AllowMissingFormatter {
7576
l.Debugf("formatter not found: %v", name)
7677
// remove this formatter
7778
delete(cfg.Formatters, name)
@@ -82,7 +83,7 @@ func (f *Format) Run() error {
8283

8384
ctx = format.RegisterFormatters(ctx, cfg.Formatters)
8485

85-
if err = cache.Open(Cli.TreeRoot, Cli.ClearCache); err != nil {
86+
if err = cache.Open(Cli.TreeRoot, Cli.ClearCache, cfg.Formatters); err != nil {
8687
return err
8788
}
8889

@@ -110,12 +111,15 @@ func (f *Format) Run() error {
110111
eg.Go(func() error {
111112
batchSize := 1024
112113
batch := make([]string, batchSize)
114+
batch = batch[:0]
113115

114116
var pending, completed, changes int
115117

116118
LOOP:
117119
for {
118120
select {
121+
case <-ctx.Done():
122+
return ctx.Err()
119123
case _, ok := <-pendingCh:
120124
if ok {
121125
pending += 1

0 commit comments

Comments
 (0)