Skip to content

Commit 16cdcf8

Browse files
committed
Use inode number and size for more robust entries change tracking
1 parent a633577 commit 16cdcf8

File tree

3 files changed

+67
-19
lines changed

3 files changed

+67
-19
lines changed

tools/disk_cache/api.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,14 @@ type DiskCache struct {
2525
Path string
2626
MaxSize int64
2727

28-
lock_file *os.File
29-
lock_mutex sync.Mutex
30-
entries Metadata
31-
entry_map map[string]*Entry
32-
entries_mod_time time.Time
33-
entries_dirty bool
34-
get_dir string
35-
read_count int
28+
lock_file *os.File
29+
lock_mutex sync.Mutex
30+
entries Metadata
31+
entry_map map[string]*Entry
32+
entries_last_read_state *file_state
33+
entries_dirty bool
34+
get_dir string
35+
read_count int
3636
}
3737

3838
func NewDiskCache(path string, max_size int64) (dc *DiskCache, err error) {

tools/disk_cache/implementation.go

Lines changed: 58 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package disk_cache
22

33
import (
4+
"bytes"
45
"crypto/sha256"
56
"encoding/hex"
67
"encoding/json"
@@ -12,11 +13,51 @@ import (
1213
"slices"
1314
"time"
1415

16+
"golang.org/x/sys/unix"
17+
1518
"github.com/kovidgoyal/kitty/tools/utils"
1619
)
1720

1821
var _ = fmt.Print
1922

23+
type file_state struct {
24+
Size int64
25+
ModTime time.Time
26+
Inode uint64
27+
}
28+
29+
func (s *file_state) equal(o *file_state) bool {
30+
return o != nil && s.Size == o.Size && s.ModTime.Equal(o.ModTime) && s.Inode == o.Inode
31+
}
32+
33+
func get_file_state(fi fs.FileInfo) *file_state {
34+
// The Sys() method returns the underlying data source (can be nil).
35+
// For Unix-like systems, it's a *syscall.Stat_t.
36+
stat, ok := fi.Sys().(*unix.Stat_t)
37+
if !ok {
38+
// For non-Unix systems, you might not have an inode.
39+
// In that case, you can fall back to using only size and mod time.
40+
return &file_state{
41+
Size: fi.Size(),
42+
ModTime: fi.ModTime(),
43+
Inode: 0, // Inode not available
44+
}
45+
}
46+
return &file_state{
47+
Size: fi.Size(),
48+
ModTime: fi.ModTime(),
49+
Inode: stat.Ino,
50+
}
51+
}
52+
53+
func get_file_state_from_path(path string) (*file_state, error) {
54+
if s, err := os.Stat(path); err != nil {
55+
return nil, err
56+
} else {
57+
return get_file_state(s), nil
58+
}
59+
}
60+
2061
func new_disk_cache(path string, max_size int64) (dc *DiskCache, err error) {
2162
if path, err = filepath.Abs(path); err != nil {
2263
return
@@ -92,16 +133,20 @@ func (dc *DiskCache) write_entries_if_dirty() (err error) {
92133
if !dc.entries_dirty {
93134
return
94135
}
136+
path := dc.entries_path()
95137
defer func() {
96138
if err == nil {
97139
dc.entries_dirty = false
98-
dc.entries_mod_time = time.Now()
140+
if s, serr := get_file_state_from_path(path); serr == nil {
141+
dc.entries_last_read_state = s
142+
}
99143
}
100144
}()
101145
if d, err := json.Marshal(dc.entries); err != nil {
102146
return err
103147
} else {
104-
return os.WriteFile(dc.entries_path(), d, 0o600)
148+
// use an atomic write so that the inode number changes
149+
return utils.AtomicWriteFile(path, bytes.NewReader(d), 0o600)
105150
}
106151
}
107152

@@ -156,14 +201,15 @@ func (dc *DiskCache) rebuild_entries() error {
156201
}
157202

158203
func (dc *DiskCache) ensure_entries() error {
159-
needed := dc.entry_map == nil
204+
needed := dc.entry_map == nil || dc.entries_last_read_state == nil
160205
path := dc.entries_path()
161-
var stat_result fs.FileInfo
206+
var fstate *file_state
162207
if !needed {
163-
if s, err := os.Stat(path); err == nil && s.ModTime().After(dc.entries_mod_time) {
164-
needed = true
165-
stat_result = s
166-
dc.entries_mod_time = s.ModTime()
208+
if s, err := get_file_state_from_path(path); err == nil {
209+
fstate = s
210+
if !s.equal(dc.entries_last_read_state) {
211+
needed = true
212+
}
167213
}
168214
}
169215
if needed {
@@ -181,11 +227,12 @@ func (dc *DiskCache) ensure_entries() error {
181227
// corrupted data
182228
dc.rebuild_entries()
183229
} else {
184-
if stat_result == nil {
185-
if s, err := os.Stat(path); err == nil {
186-
dc.entries_mod_time = s.ModTime()
230+
if fstate == nil {
231+
if s, err := get_file_state_from_path(path); err == nil {
232+
fstate = s
187233
}
188234
}
235+
dc.entries_last_read_state = fstate
189236
}
190237
dc.entry_map = make(map[string]*Entry)
191238
for _, e := range dc.entries.SortedEntries {

tools/disk_cache/implementation_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ func TestDiskCache(t *testing.T) {
7777
arc(dc, 1) // because dc2.Get() will have updated the file
7878
arc(dc2, 1)
7979
ak("k1")
80+
arc(dc2, 2) // because dc.Add() will have updated the file
8081
dc2.Add("k2", map[string][]byte{"1": []byte("123456789")})
8182
arc(dc, 1)
8283
arc(dc2, 2)

0 commit comments

Comments
 (0)