Skip to content

Commit d591989

Browse files
committed
feat(caching): content based caching to discovery, loader, output
1 parent 04e7e82 commit d591989

8 files changed

Lines changed: 360 additions & 35 deletions

File tree

.github/workflows/tests.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,10 @@ jobs:
2020
matrix:
2121
os: [ubuntu-latest, macos-latest, windows-latest]
2222
go-version: [1.25.x]
23+
cache-mode: [mtime, content]
2324
runs-on: ${{ matrix.os }}
25+
env:
26+
WIRE_CACHE_MODE: ${{ matrix.cache-mode }}
2427
steps:
2528
- name: Install Go
2629
uses: actions/setup-go@v2
@@ -34,9 +37,6 @@ jobs:
3437
shell: bash
3538
run: 'internal/runtests.sh'
3639

37-
- name: Run tests
38-
run: go test ./... -v
39-
4040
codecov:
4141
runs-on: ubuntu-latest
4242

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package cachepolicy
2+
3+
import "os"
4+
5+
const (
6+
ModeEnv = "WIRE_CACHE_MODE"
7+
ModeMTime = "mtime"
8+
ModeContent = "content"
9+
)
10+
11+
var mode = loadMode()
12+
13+
func UseFileContent() bool {
14+
return mode == ModeContent
15+
}
16+
17+
func ModeLabel() string {
18+
return mode
19+
}
20+
21+
func SetForTest(next string) func() {
22+
prev := mode
23+
mode = normalizeMode(next)
24+
return func() {
25+
mode = prev
26+
}
27+
}
28+
29+
func loadMode() string {
30+
return normalizeMode(os.Getenv(ModeEnv))
31+
}
32+
33+
func normalizeMode(value string) string {
34+
switch value {
35+
case ModeContent:
36+
return ModeContent
37+
default:
38+
return ModeMTime
39+
}
40+
}

internal/loader/artifact_cache.go

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,12 @@ import (
2424
"os"
2525
"path/filepath"
2626
"runtime"
27+
"strconv"
2728

2829
"golang.org/x/tools/go/gcexportdata"
2930

3031
"github.com/goforj/wire/internal/cachepaths"
32+
"github.com/goforj/wire/internal/cachepolicy"
3133
)
3234

3335
const (
@@ -57,25 +59,25 @@ func loaderArtifactPath(env []string, meta *packageMeta, isLocal bool) (string,
5759

5860
func loaderArtifactKey(meta *packageMeta, isLocal bool) (string, error) {
5961
sum := sha256.New()
60-
sum.Write([]byte("wire-loader-artifact-v4\n"))
62+
sum.Write([]byte("wire-loader-artifact-v5\n"))
6163
sum.Write([]byte(runtime.Version()))
6264
sum.Write([]byte{'\n'})
65+
sum.Write([]byte(cachepolicy.ModeLabel()))
66+
sum.Write([]byte{'\n'})
6367
sum.Write([]byte(meta.ImportPath))
6468
sum.Write([]byte{'\n'})
6569
sum.Write([]byte(meta.Name))
6670
sum.Write([]byte{'\n'})
71+
useFileContent := cachepolicy.UseFileContent()
6772
if !isLocal {
6873
sum.Write([]byte(meta.Export))
6974
sum.Write([]byte{'\n'})
7075
if meta.Export != "" {
71-
h, err := hashFileContent(meta.Export)
72-
if err != nil {
76+
if err := hashFileInput(sum, meta.Export, useFileContent); err != nil {
7377
return "", err
7478
}
75-
sum.Write([]byte(h))
76-
sum.Write([]byte{'\n'})
7779
} else {
78-
if err := hashMetaFiles(sum, metaFiles(meta)); err != nil {
80+
if err := hashMetaFiles(sum, metaFiles(meta), useFileContent); err != nil {
7981
return "", err
8082
}
8183
}
@@ -85,7 +87,7 @@ func loaderArtifactKey(meta *packageMeta, isLocal bool) (string, error) {
8587
}
8688
return hex.EncodeToString(sum.Sum(nil)), nil
8789
}
88-
if err := hashMetaFiles(sum, metaFiles(meta)); err != nil {
90+
if err := hashMetaFiles(sum, metaFiles(meta), useFileContent); err != nil {
8991
return "", err
9092
}
9193
return hex.EncodeToString(sum.Sum(nil)), nil
@@ -101,18 +103,36 @@ func hashFileContent(path string) (string, error) {
101103
return hex.EncodeToString(h[:]), nil
102104
}
103105

104-
// hashMetaFiles writes content-based hashes for each file into sum.
105-
func hashMetaFiles(sum io.Writer, names []string) error {
106+
// hashMetaFiles writes file identity inputs for each file into sum.
107+
func hashMetaFiles(sum io.Writer, names []string, useFileContent bool) error {
106108
for _, name := range names {
107109
sum.Write([]byte(name))
108110
sum.Write([]byte{'\n'})
109-
h, err := hashFileContent(name)
111+
if err := hashFileInput(sum, name, useFileContent); err != nil {
112+
return err
113+
}
114+
}
115+
return nil
116+
}
117+
118+
func hashFileInput(sum io.Writer, path string, useFileContent bool) error {
119+
if useFileContent {
120+
h, err := hashFileContent(path)
110121
if err != nil {
111122
return err
112123
}
113124
sum.Write([]byte(h))
114125
sum.Write([]byte{'\n'})
126+
return nil
127+
}
128+
info, err := os.Stat(path)
129+
if err != nil {
130+
return err
115131
}
132+
sum.Write([]byte(strconv.FormatInt(info.Size(), 10)))
133+
sum.Write([]byte{'\n'})
134+
sum.Write([]byte(strconv.FormatInt(info.ModTime().UnixNano(), 10)))
135+
sum.Write([]byte{'\n'})
116136
return nil
117137
}
118138

internal/loader/discovery_cache.go

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"sort"
1414

1515
"github.com/goforj/wire/internal/cachepaths"
16+
"github.com/goforj/wire/internal/cachepolicy"
1617
)
1718

1819
type discoveryCacheEntry struct {
@@ -32,7 +33,7 @@ type discoveryLocalPackage struct {
3233
type discoveryFileMeta struct {
3334
Path string
3435
Size int64
35-
ModTime int64 // deprecated: kept for gob compat, not used for matching
36+
ModTime int64
3637
ContentHash string // sha256 of file content
3738
IsDir bool
3839
}
@@ -47,7 +48,7 @@ type discoveryFileFingerprint struct {
4748
Hash string
4849
}
4950

50-
const discoveryCacheVersion = 4
51+
const discoveryCacheVersion = 5
5152

5253
func readDiscoveryCache(req goListRequest) (map[string]*packageMeta, bool) {
5354
entry, err := loadDiscoveryCacheEntry(req)
@@ -62,6 +63,7 @@ func readDiscoveryCache(req goListRequest) (map[string]*packageMeta, bool) {
6263

6364
func buildDiscoveryCacheEntry(req goListRequest, meta map[string]*packageMeta) (*discoveryCacheEntry, error) {
6465
workspace := detectModuleRoot(req.WD)
66+
useFileContent := cachepolicy.UseFileContent()
6567
entry := &discoveryCacheEntry{
6668
Version: discoveryCacheVersion,
6769
Meta: meta,
@@ -73,7 +75,7 @@ func buildDiscoveryCacheEntry(req goListRequest, meta map[string]*packageMeta) (
7375
filepath.Join(workspace, "go.work.sum"),
7476
}
7577
for _, name := range global {
76-
if fm, ok := statDiscoveryFile(name); ok {
78+
if fm, ok := statDiscoveryFile(name, useFileContent); ok {
7779
entry.Global = append(entry.Global, fm)
7880
}
7981
}
@@ -106,8 +108,9 @@ func validateDiscoveryCacheEntry(entry *discoveryCacheEntry) bool {
106108
if entry == nil || entry.Version != discoveryCacheVersion {
107109
return false
108110
}
111+
useFileContent := cachepolicy.UseFileContent()
109112
for _, fm := range entry.Global {
110-
if !matchesDiscoveryFile(fm) {
113+
if !matchesDiscoveryFile(fm, useFileContent) {
111114
return false
112115
}
113116
}
@@ -133,6 +136,7 @@ func discoveryCachePath(req goListRequest) (string, error) {
133136
}
134137
sumReq := struct {
135138
Version int
139+
CacheMode string
136140
WD string
137141
Tags string
138142
Patterns []string
@@ -141,6 +145,7 @@ func discoveryCachePath(req goListRequest) (string, error) {
141145
Go string
142146
}{
143147
Version: discoveryCacheVersion,
148+
CacheMode: cachepolicy.ModeLabel(),
144149
WD: canonicalLoaderPath(req.WD),
145150
Tags: req.Tags,
146151
Patterns: append([]string(nil), req.Patterns...),
@@ -188,13 +193,13 @@ func saveDiscoveryCacheEntry(req goListRequest, entry *discoveryCacheEntry) erro
188193
return gob.NewEncoder(f).Encode(entry)
189194
}
190195

191-
func statDiscoveryFile(path string) (discoveryFileMeta, bool) {
196+
func statDiscoveryFile(path string, useFileContent bool) (discoveryFileMeta, bool) {
192197
info, err := os.Stat(path)
193198
if err != nil {
194199
return discoveryFileMeta{}, false
195200
}
196201
h := ""
197-
if !info.IsDir() {
202+
if !info.IsDir() && useFileContent {
198203
var err error
199204
h, err = hashFileContent(path)
200205
if err != nil {
@@ -204,17 +209,24 @@ func statDiscoveryFile(path string) (discoveryFileMeta, bool) {
204209
return discoveryFileMeta{
205210
Path: canonicalLoaderPath(path),
206211
Size: info.Size(),
212+
ModTime: info.ModTime().UnixNano(),
207213
ContentHash: h,
208214
IsDir: info.IsDir(),
209215
}, true
210216
}
211217

212-
func matchesDiscoveryFile(fm discoveryFileMeta) bool {
213-
cur, ok := statDiscoveryFile(fm.Path)
218+
func matchesDiscoveryFile(fm discoveryFileMeta, useFileContent bool) bool {
219+
cur, ok := statDiscoveryFile(fm.Path, useFileContent)
214220
if !ok {
215221
return false
216222
}
217-
return cur.ContentHash == fm.ContentHash && cur.IsDir == fm.IsDir
223+
if cur.IsDir != fm.IsDir {
224+
return false
225+
}
226+
if useFileContent {
227+
return cur.ContentHash == fm.ContentHash
228+
}
229+
return cur.Size == fm.Size && cur.ModTime == fm.ModTime
218230
}
219231

220232
func statDiscoveryDir(path string) (discoveryDirMeta, bool) {

internal/loader/discovery_cache_test.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import (
44
"os"
55
"path/filepath"
66
"testing"
7+
8+
"github.com/goforj/wire/internal/cachepolicy"
79
)
810

911
func TestDiscoveryFingerprintIgnoresBodyOnlyEdits(t *testing.T) {
@@ -124,3 +126,67 @@ func TestDiscoveryDirDetectsFileSetChange(t *testing.T) {
124126
t.Fatalf("directory metadata did not detect added file")
125127
}
126128
}
129+
130+
func TestMatchesDiscoveryFileDefaultModTimeIgnoresContentOnlyChangeWithSameMetadata(t *testing.T) {
131+
restore := cachepolicy.SetForTest(cachepolicy.ModeMTime)
132+
defer restore()
133+
134+
dir := t.TempDir()
135+
path := filepath.Join(dir, "go.mod")
136+
write := func(content string) {
137+
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
138+
t.Fatal(err)
139+
}
140+
}
141+
142+
write("module example.com/one\n")
143+
info, err := os.Stat(path)
144+
if err != nil {
145+
t.Fatal(err)
146+
}
147+
meta, ok := statDiscoveryFile(path, false)
148+
if !ok {
149+
t.Fatalf("statDiscoveryFile(%q) failed", path)
150+
}
151+
152+
write("module example.com/two\n")
153+
if err := os.Chtimes(path, info.ModTime(), info.ModTime()); err != nil {
154+
t.Fatal(err)
155+
}
156+
157+
if !matchesDiscoveryFile(meta, false) {
158+
t.Fatalf("default mod-time matching rejected unchanged size/modtime metadata")
159+
}
160+
}
161+
162+
func TestMatchesDiscoveryFileContentModeDetectsContentOnlyChangeWithSameMetadata(t *testing.T) {
163+
restore := cachepolicy.SetForTest(cachepolicy.ModeContent)
164+
defer restore()
165+
166+
dir := t.TempDir()
167+
path := filepath.Join(dir, "go.mod")
168+
write := func(content string) {
169+
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
170+
t.Fatal(err)
171+
}
172+
}
173+
174+
write("module example.com/one\n")
175+
info, err := os.Stat(path)
176+
if err != nil {
177+
t.Fatal(err)
178+
}
179+
meta, ok := statDiscoveryFile(path, true)
180+
if !ok {
181+
t.Fatalf("statDiscoveryFile(%q) failed", path)
182+
}
183+
184+
write("module example.com/two\n")
185+
if err := os.Chtimes(path, info.ModTime(), info.ModTime()); err != nil {
186+
t.Fatal(err)
187+
}
188+
189+
if matchesDiscoveryFile(meta, true) {
190+
t.Fatalf("content-based matching accepted changed file content")
191+
}
192+
}

0 commit comments

Comments
 (0)