返回 DeepSeek-Reasonix
file_compensate.go
根目录 / internal / extension / file_compensate.go
1 package extension
2
3 import (
4 "fmt"
5 "os"
6 "path/filepath"
7 "sync"
8 )
9
10 const (
11 // Prior bytes are an in-process recovery aid, not an unbounded file cache.
12 // Keep large writes compensatable when practical while bounding one owner.
13 defaultFilePriorMaxBytes = 32 << 20
14 defaultFilePriorMaxEntryBytes = 8 << 20
15 )
16
17 // FilePriorStore holds prior file contents for compensatable write_file
18 // receipts so recovery can restore them (never claim success without apply).
19 type FilePriorStore struct {
20 mu sync.Mutex
21 byID map[string]filePrior
22 retainedBytes int
23 maxBytes int
24 maxEntryBytes int
25 }
26
27 type filePrior struct {
28 Path string
29 Content []byte
30 Existed bool
31 }
32
33 // DefaultFilePriorStore belongs to the compatibility runtime owner.
34 var DefaultFilePriorStore = DefaultRuntimeOwner.FilePriors
35
36 // NewFilePriorStore returns an empty store.
37 func NewFilePriorStore() *FilePriorStore {
38 return newFilePriorStore(defaultFilePriorMaxBytes, defaultFilePriorMaxEntryBytes)
39 }
40
41 func newFilePriorStore(maxBytes, maxEntryBytes int) *FilePriorStore {
42 return &FilePriorStore{
43 byID: make(map[string]filePrior),
44 maxBytes: maxBytes,
45 maxEntryBytes: maxEntryBytes,
46 }
47 }
48
49 // Capture records prior content for path under receipt id.
50 // It returns false when retaining the prior would exceed the store budget.
51 func (s *FilePriorStore) Capture(id, path string, content []byte, existed bool) bool {
52 if s == nil || id == "" || path == "" {
53 return false
54 }
55 if s.maxEntryBytes > 0 && len(content) > s.maxEntryBytes {
56 return false
57 }
58 s.mu.Lock()
59 old, hadOld := s.byID[id]
60 oldBytes := 0
61 if hadOld {
62 oldBytes = len(old.Content)
63 }
64 available := s.retainedBytes - oldBytes
65 if s.maxBytes > 0 && available+len(content) > s.maxBytes {
66 s.mu.Unlock()
67 return false
68 }
69 cp := make([]byte, len(content))
70 copy(cp, content)
71 s.byID[id] = filePrior{Path: path, Content: cp, Existed: existed}
72 s.retainedBytes = available + len(cp)
73 s.mu.Unlock()
74 return true
75 }
76
77 // Compensate restores the prior content (or removes a created file). Returns
78 // error if unknown or IO fails; updates receipt compensation status when store
79 // is DefaultReceiptStore-linked via caller.
80 func (s *FilePriorStore) Compensate(id string) error {
81 if s == nil {
82 return fmt.Errorf("extension: nil file prior store")
83 }
84 s.mu.Lock()
85 prior, ok := s.byID[id]
86 s.mu.Unlock()
87 if !ok {
88 return fmt.Errorf("extension: no prior captured for %s", id)
89 }
90 if !prior.Existed {
91 if err := os.Remove(prior.Path); err != nil && !os.IsNotExist(err) {
92 return err
93 }
94 return nil
95 }
96 if err := os.MkdirAll(filepath.Dir(prior.Path), 0o755); err != nil {
97 return err
98 }
99 return os.WriteFile(prior.Path, prior.Content, 0o644)
100 }
101
102 // Forget drops a prior entry after successful compensation.
103 func (s *FilePriorStore) Forget(id string) {
104 if s == nil {
105 return
106 }
107 s.mu.Lock()
108 if prior, ok := s.byID[id]; ok {
109 s.retainedBytes -= len(prior.Content)
110 if s.retainedBytes < 0 {
111 s.retainedBytes = 0
112 }
113 delete(s.byID, id)
114 }
115 s.mu.Unlock()
116 }
117
118 // ApplyFileWriteCompensation restores prior content and marks the receipt applied.
119 func ApplyFileWriteCompensation(receiptID string) error {
120 return RuntimeOwnerOrDefault(nil).ApplyFileWriteCompensation(receiptID)
121 }
122
122 lines GO