Skip to content

Commit 1031651

Browse files
committed
FIX: retry usage configuration after read or close failures
1 parent 50e284d commit 1031651

2 files changed

Lines changed: 83 additions & 6 deletions

File tree

‎internal/codex/usage_coverage.go‎

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -134,16 +134,26 @@ func (r *LiveUsageReader) localAsyncCompaction(path, session string) (bool, stri
134134
diagnostics.Error("local-config", "open", err, fields)
135135
return false, "background usage configuration could not be read"
136136
}
137+
return r.readLocalAsyncCompaction(file, info, fields)
138+
}
139+
140+
func (r *LiveUsageReader) readLocalAsyncCompaction(file io.ReadCloser, info os.FileInfo, fields diagnostics.Fields) (bool, string) {
137141
data, err := io.ReadAll(io.LimitReader(file, 1<<20+1))
138-
if closeErr := file.Close(); closeErr != nil {
142+
closeErr := file.Close()
143+
if closeErr != nil {
139144
diagnostics.Error("local-config", "close", closeErr, fields)
140145
}
141-
check := localFeatureCheck{info: info}
142-
var config map[string]any
143146
if err != nil {
144147
diagnostics.Error("local-config", "read", err, fields)
145-
check.issue = "background usage configuration could not be verified"
146-
} else if len(data) > 1<<20 {
148+
}
149+
// Transient filesystem failures must be retried, not cached as a valid
150+
// configuration (or as a persistent failure for otherwise unchanged bytes).
151+
if err != nil || closeErr != nil {
152+
return false, "background usage configuration could not be verified"
153+
}
154+
check := localFeatureCheck{info: info}
155+
var config map[string]any
156+
if len(data) > 1<<20 {
147157
diagnostics.Error("local-config", "configuration exceeds 1 MiB limit", nil, fields)
148158
check.issue = "background usage configuration could not be verified"
149159
} else if parseErr := toml.Unmarshal(data, &config); parseErr != nil {
@@ -157,7 +167,7 @@ func (r *LiveUsageReader) localAsyncCompaction(path, session string) (bool, stri
157167
if len(r.localFeatureChecks) >= 256 {
158168
clear(r.localFeatureChecks)
159169
}
160-
r.localFeatureChecks[path] = check
170+
r.localFeatureChecks[fields.Path] = check
161171
return check.enabled, check.issue
162172
}
163173

‎internal/codex/usage_coverage_logging_test.go‎

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,82 @@
11
package codex
22

33
import (
4+
"io"
45
"os"
56
"path/filepath"
67
"runtime"
78
"strings"
89
"testing"
10+
"testing/iotest"
911

1012
"github.com/merefield/codexometer/internal/diagnostics"
1113
)
1214

15+
type coverageReadCloser struct {
16+
io.Reader
17+
closeErr error
18+
closed bool
19+
}
20+
21+
func (f *coverageReadCloser) Close() error {
22+
f.closed = true
23+
return f.closeErr
24+
}
25+
26+
func TestUsageCoverageReadAndCloseFailuresAreLoggedAndRetried(t *testing.T) {
27+
for _, failure := range []string{"read", "close", "read and close"} {
28+
t.Run(failure, func(t *testing.T) {
29+
dir := t.TempDir()
30+
path, logPath := filepath.Join(dir, "config.toml"), filepath.Join(dir, "error.log")
31+
const config = "[features]\nasync_compaction = true\n"
32+
if err := os.WriteFile(path, []byte(config), 0600); err != nil {
33+
t.Fatal(err)
34+
}
35+
info, err := os.Stat(path)
36+
if err != nil {
37+
t.Fatal(err)
38+
}
39+
closeLog, err := diagnostics.Install(logPath, false)
40+
if err != nil {
41+
t.Fatal(err)
42+
}
43+
t.Cleanup(func() { closeLog() })
44+
file := &coverageReadCloser{Reader: strings.NewReader(config)}
45+
if strings.Contains(failure, "read") {
46+
file.Reader = iotest.ErrReader(os.ErrPermission)
47+
}
48+
if strings.Contains(failure, "close") {
49+
file.closeErr = os.ErrPermission
50+
}
51+
r := &LiveUsageReader{localFeatureChecks: make(map[string]localFeatureCheck)}
52+
enabled, issue := r.readLocalAsyncCompaction(file, info, diagnostics.Fields{Path: path, Session: "thread"})
53+
if enabled || issue != "background usage configuration could not be verified" || !file.closed {
54+
t.Fatal("filesystem failure did not block verification and close the file", enabled, issue, file.closed)
55+
}
56+
if len(r.localFeatureChecks) != 0 {
57+
t.Fatal("failed filesystem operation was cached", r.localFeatureChecks)
58+
}
59+
if err := closeLog(); err != nil {
60+
t.Fatal(err)
61+
}
62+
data, err := os.ReadFile(logPath)
63+
if err != nil {
64+
t.Fatal(err)
65+
}
66+
for _, operation := range []string{"read", "close"} {
67+
if strings.Contains(failure, operation) && !strings.Contains(string(data), `"msg":"`+operation+`"`) {
68+
t.Fatal("filesystem failure was not logged", operation, string(data))
69+
}
70+
}
71+
// Retry the same path and unchanged metadata after the transient
72+
// failure. A successful read must now detect and cache the flag.
73+
if enabled, issue := r.localAsyncCompaction(path, "thread"); !enabled || issue != "" || !r.localFeatureChecks[path].enabled {
74+
t.Fatal("unchanged configuration was not successfully retried", enabled, issue)
75+
}
76+
})
77+
}
78+
}
79+
1380
func TestUsageCoverageLogsFilesystemCauseWithoutChangingTheGuard(t *testing.T) {
1481
home := t.TempDir()
1582
logPath, config := filepath.Join(home, "error.log"), filepath.Join(home, "config.toml")

0 commit comments

Comments
 (0)