Skip to content

Commit 64799cc

Browse files
authored
Merge pull request #593 from numtide/fix/concurrent-invocation
fix: force-remove db file when clearing cache
2 parents 10a9128 + 8c25148 commit 64799cc

File tree

4 files changed

+124
-41
lines changed

4 files changed

+124
-41
lines changed

cmd/format/format.go

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,13 @@ func Run(v *viper.Viper, statz *stats.Stats, cmd *cobra.Command, paths []string)
7777
}()
7878
}
7979

80+
// Remove the cache first before potentially opening a new one.
81+
if cfg.ClearCache {
82+
if err := cache.Remove(cfg.TreeRoot); err != nil {
83+
return fmt.Errorf("failed to clear cache: %w", err)
84+
}
85+
}
86+
8087
var db *bolt.DB
8188

8289
// open the db unless --no-cache was specified
@@ -94,15 +101,6 @@ func Run(v *viper.Viper, statz *stats.Stats, cmd *cobra.Command, paths []string)
94101
}()
95102
}
96103

97-
if db != nil {
98-
// clear the cache if desired
99-
if cfg.ClearCache {
100-
if err = cache.Clear(db); err != nil {
101-
return fmt.Errorf("failed to clear cache: %w", err)
102-
}
103-
}
104-
}
105-
106104
// create an overall app context
107105
ctx, cancel := context.WithCancel(context.Background())
108106
defer cancel()

cmd/root_test.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
"github.com/numtide/treefmt/v2/walk"
2525
cp "github.com/otiai10/copy"
2626
"github.com/stretchr/testify/require"
27+
"golang.org/x/sync/errgroup"
2728
)
2829

2930
func TestOnUnmatched(t *testing.T) {
@@ -2141,6 +2142,79 @@ func TestProjectRootIsSymlink(t *testing.T) {
21412142
)
21422143
}
21432144

2145+
func TestConcurrentInvocation(t *testing.T) {
2146+
as := require.New(t)
2147+
2148+
tempDir := test.TempExamples(t)
2149+
configPath := filepath.Join(tempDir, "/treefmt.toml")
2150+
2151+
test.ChangeWorkDir(t, tempDir)
2152+
2153+
cfg := &config.Config{
2154+
FormatterConfigs: map[string]*config.Formatter{
2155+
"echo": {
2156+
Command: "echo",
2157+
Includes: []string{"*"},
2158+
},
2159+
"slow": {
2160+
Command: "test-fmt-delayed-append",
2161+
// connect timeout for the db is 1 second
2162+
// wait 2 seconds before appending ' ' to each provided path
2163+
Options: []string{"2", " "},
2164+
Includes: []string{"*"},
2165+
},
2166+
},
2167+
}
2168+
2169+
eg := errgroup.Group{}
2170+
2171+
// concurrent invocation with one slow instance and one not
2172+
2173+
eg.Go(func() error {
2174+
treefmt(t,
2175+
withArgs("--formatters", "slow"),
2176+
withConfig(configPath, cfg),
2177+
withNoError(t),
2178+
)
2179+
2180+
return nil
2181+
})
2182+
2183+
time.Sleep(500 * time.Millisecond)
2184+
2185+
treefmt(t,
2186+
withArgs("--formatters", "echo"),
2187+
withConfig(configPath, cfg),
2188+
withError(func(as *require.Assertions, err error) {
2189+
as.ErrorContains(err, "failed to open cache")
2190+
}),
2191+
)
2192+
2193+
as.NoError(eg.Wait())
2194+
2195+
// concurrent invocation with one slow instance and one configured to clear the cache
2196+
2197+
eg.Go(func() error {
2198+
treefmt(t,
2199+
withArgs("--formatters", "slow"),
2200+
withConfig(configPath, cfg),
2201+
withNoError(t),
2202+
)
2203+
2204+
return nil
2205+
})
2206+
2207+
time.Sleep(500 * time.Millisecond)
2208+
2209+
treefmt(t,
2210+
withArgs("-c", "--formatters", "echo"),
2211+
withConfig(configPath, cfg),
2212+
withNoError(t),
2213+
)
2214+
2215+
as.NoError(eg.Wait())
2216+
}
2217+
21442218
type options struct {
21452219
args []string
21462220
env map[string]string

nix/packages/treefmt/formatters.nix

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,4 +45,16 @@ with pkgs; [
4545
done
4646
'';
4747
})
48+
(pkgs.writeShellApplication {
49+
name = "test-fmt-delayed-append";
50+
text = ''
51+
DELAY="$1"
52+
shift
53+
54+
# sleep first
55+
sleep "$DELAY"
56+
57+
test-fmt-append "$@"
58+
'';
59+
})
4860
]

walk/cache/cache.go

Lines changed: 31 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"crypto/sha256"
55
"encoding/hex"
66
"fmt"
7+
"os"
78
"time"
89

910
"github.com/adrg/xdg"
@@ -14,26 +15,33 @@ const (
1415
bucketPaths = "paths"
1516
)
1617

17-
func Open(root string) (*bolt.DB, error) {
18-
var (
19-
err error
20-
path string
21-
)
22-
23-
// Otherwise, the database will be located in `XDG_CACHE_DIR/treefmt/eval-cache/<name>.db`, where <name> is
24-
// determined by hashing the treeRoot path.
25-
// This associates a given treeRoot with a given instance of the cache.
18+
// Path returns a unique local cache file path for the given root string, using its SHA-256 hash.
19+
func Path(root string) (string, error) {
2620
digest := sha256.Sum256([]byte(root))
2721

2822
name := hex.EncodeToString(digest[:])
29-
if path, err = xdg.CacheFile(fmt.Sprintf("treefmt/eval-cache/%v.db", name)); err != nil {
30-
return nil, fmt.Errorf("could not resolve local path for the cache: %w", err)
23+
24+
path, err := xdg.CacheFile(fmt.Sprintf("treefmt/eval-cache/%v.db", name))
25+
if err != nil {
26+
return "", fmt.Errorf("could not resolve local path for the cache: %w", err)
27+
}
28+
29+
return path, nil
30+
}
31+
32+
// Open initialises and opens a Bolt database for the specified root path.
33+
// Returns a pointer to the opened database or an error if initialisation fails.
34+
func Open(root string) (*bolt.DB, error) {
35+
// determine the db location
36+
path, err := Path(root)
37+
if err != nil {
38+
return nil, err
3139
}
3240

3341
// open db
3442
db, err := bolt.Open(path, 0o600, &bolt.Options{Timeout: 1 * time.Second})
3543
if err != nil {
36-
return nil, fmt.Errorf("failed to open cache db: %w", err)
44+
return nil, fmt.Errorf("failed to open cache db at %s: %w", path, err)
3745
}
3846

3947
// ensure bucket exist
@@ -56,28 +64,19 @@ func PathsBucket(tx *bolt.Tx) *bolt.Bucket {
5664
return tx.Bucket([]byte("paths"))
5765
}
5866

59-
func deleteAll(bucket *bolt.Bucket) error {
60-
c := bucket.Cursor()
61-
for k, v := c.First(); !(k == nil && v == nil); k, v = c.Next() {
62-
if err := c.Delete(); err != nil {
63-
return fmt.Errorf("failed to remove cache entry for key %s: %w", string(k), err)
64-
}
67+
func Remove(root string) error {
68+
// determine the db location
69+
path, err := Path(root)
70+
if err != nil {
71+
return err
6572
}
6673

67-
return nil
68-
}
69-
70-
func Clear(db *bolt.DB) error {
71-
err := db.Update(func(tx *bolt.Tx) error {
72-
err := deleteAll(PathsBucket(tx))
73-
if err != nil {
74-
return fmt.Errorf("failed to clear cache: %w", err)
75-
}
76-
77-
return nil
78-
})
79-
if err != nil {
80-
return fmt.Errorf("failed to clear cache: %w", err)
74+
// Remove any db which might already exist.
75+
// If a treefmt process is currently running with a db open at the same location, it will continue to function
76+
// as normal, however, when it exits the disk space its inode was referencing will be reclaimed.
77+
// This will not work on Windows if we ever support it.
78+
if err = os.Remove(path); !(err == nil || os.IsNotExist(err)) {
79+
return fmt.Errorf("failed to remove cache db at %s: %w", path, err)
8180
}
8281

8382
return nil

0 commit comments

Comments
 (0)