Use inode number and size for more robust entries change tracking

This commit is contained in:
Kovid Goyal
2025-10-08 07:28:02 +05:30
parent a6335777d9
commit 16cdcf8cf8
3 changed files with 67 additions and 19 deletions

View File

@@ -25,14 +25,14 @@ type DiskCache struct {
Path string Path string
MaxSize int64 MaxSize int64
lock_file *os.File lock_file *os.File
lock_mutex sync.Mutex lock_mutex sync.Mutex
entries Metadata entries Metadata
entry_map map[string]*Entry entry_map map[string]*Entry
entries_mod_time time.Time entries_last_read_state *file_state
entries_dirty bool entries_dirty bool
get_dir string get_dir string
read_count int read_count int
} }
func NewDiskCache(path string, max_size int64) (dc *DiskCache, err error) { func NewDiskCache(path string, max_size int64) (dc *DiskCache, err error) {

View File

@@ -1,6 +1,7 @@
package disk_cache package disk_cache
import ( import (
"bytes"
"crypto/sha256" "crypto/sha256"
"encoding/hex" "encoding/hex"
"encoding/json" "encoding/json"
@@ -12,11 +13,51 @@ import (
"slices" "slices"
"time" "time"
"golang.org/x/sys/unix"
"github.com/kovidgoyal/kitty/tools/utils" "github.com/kovidgoyal/kitty/tools/utils"
) )
var _ = fmt.Print var _ = fmt.Print
type file_state struct {
Size int64
ModTime time.Time
Inode uint64
}
func (s *file_state) equal(o *file_state) bool {
return o != nil && s.Size == o.Size && s.ModTime.Equal(o.ModTime) && s.Inode == o.Inode
}
func get_file_state(fi fs.FileInfo) *file_state {
// The Sys() method returns the underlying data source (can be nil).
// For Unix-like systems, it's a *syscall.Stat_t.
stat, ok := fi.Sys().(*unix.Stat_t)
if !ok {
// For non-Unix systems, you might not have an inode.
// In that case, you can fall back to using only size and mod time.
return &file_state{
Size: fi.Size(),
ModTime: fi.ModTime(),
Inode: 0, // Inode not available
}
}
return &file_state{
Size: fi.Size(),
ModTime: fi.ModTime(),
Inode: stat.Ino,
}
}
func get_file_state_from_path(path string) (*file_state, error) {
if s, err := os.Stat(path); err != nil {
return nil, err
} else {
return get_file_state(s), nil
}
}
func new_disk_cache(path string, max_size int64) (dc *DiskCache, err error) { func new_disk_cache(path string, max_size int64) (dc *DiskCache, err error) {
if path, err = filepath.Abs(path); err != nil { if path, err = filepath.Abs(path); err != nil {
return return
@@ -92,16 +133,20 @@ func (dc *DiskCache) write_entries_if_dirty() (err error) {
if !dc.entries_dirty { if !dc.entries_dirty {
return return
} }
path := dc.entries_path()
defer func() { defer func() {
if err == nil { if err == nil {
dc.entries_dirty = false dc.entries_dirty = false
dc.entries_mod_time = time.Now() if s, serr := get_file_state_from_path(path); serr == nil {
dc.entries_last_read_state = s
}
} }
}() }()
if d, err := json.Marshal(dc.entries); err != nil { if d, err := json.Marshal(dc.entries); err != nil {
return err return err
} else { } else {
return os.WriteFile(dc.entries_path(), d, 0o600) // use an atomic write so that the inode number changes
return utils.AtomicWriteFile(path, bytes.NewReader(d), 0o600)
} }
} }
@@ -156,14 +201,15 @@ func (dc *DiskCache) rebuild_entries() error {
} }
func (dc *DiskCache) ensure_entries() error { func (dc *DiskCache) ensure_entries() error {
needed := dc.entry_map == nil needed := dc.entry_map == nil || dc.entries_last_read_state == nil
path := dc.entries_path() path := dc.entries_path()
var stat_result fs.FileInfo var fstate *file_state
if !needed { if !needed {
if s, err := os.Stat(path); err == nil && s.ModTime().After(dc.entries_mod_time) { if s, err := get_file_state_from_path(path); err == nil {
needed = true fstate = s
stat_result = s if !s.equal(dc.entries_last_read_state) {
dc.entries_mod_time = s.ModTime() needed = true
}
} }
} }
if needed { if needed {
@@ -181,11 +227,12 @@ func (dc *DiskCache) ensure_entries() error {
// corrupted data // corrupted data
dc.rebuild_entries() dc.rebuild_entries()
} else { } else {
if stat_result == nil { if fstate == nil {
if s, err := os.Stat(path); err == nil { if s, err := get_file_state_from_path(path); err == nil {
dc.entries_mod_time = s.ModTime() fstate = s
} }
} }
dc.entries_last_read_state = fstate
} }
dc.entry_map = make(map[string]*Entry) dc.entry_map = make(map[string]*Entry)
for _, e := range dc.entries.SortedEntries { for _, e := range dc.entries.SortedEntries {

View File

@@ -77,6 +77,7 @@ func TestDiskCache(t *testing.T) {
arc(dc, 1) // because dc2.Get() will have updated the file arc(dc, 1) // because dc2.Get() will have updated the file
arc(dc2, 1) arc(dc2, 1)
ak("k1") ak("k1")
arc(dc2, 2) // because dc.Add() will have updated the file
dc2.Add("k2", map[string][]byte{"1": []byte("123456789")}) dc2.Add("k2", map[string][]byte{"1": []byte("123456789")})
arc(dc, 1) arc(dc, 1)
arc(dc2, 2) arc(dc2, 2)