返回 DeepSeek-Reasonix
index.go
根目录 / internal / session / index.go
1 package session
2
3 import (
4 "context"
5 "crypto/sha256"
6 "encoding/hex"
7 "encoding/json"
8 "fmt"
9 "io"
10 "os"
11 "path/filepath"
12 "sort"
13
14 "reasonix/internal/fileutil"
15 )
16
17 const (
18 sparseIndexCodec = "reasonix.session.offset-index/v1"
19 sparseIndexInterval = 256
20 sparseIdentityBytes = 4096
21 )
22
23 type sparseIndex struct {
24 Codec string `json:"codec"`
25 LogSize int64 `json:"logSize"`
26 LogModTimeNS int64 `json:"logModTimeNs"`
27 LogIdentity string `json:"logIdentity"`
28 LastSequence uint64 `json:"lastSequence"`
29 CommitCount uint64 `json:"commitCount"`
30 Entries []sparseIndexEntry `json:"entries"`
31 partial bool
32 }
33
34 type sparseIndexEntry struct {
35 FirstSequence uint64 `json:"firstSequence"`
36 Offset int64 `json:"offset"`
37 }
38
39 func sparseIndexPath(cacheDir string) string {
40 return filepath.Join(cacheDir, "events.offset-index.json")
41 }
42
43 // loadOrBuildSparseIndex treats the index as an expendable cache. A missing or
44 // corrupt cache is rebuilt by validating the complete durable log. Failure to
45 // write the rebuilt cache does not make an otherwise readable session fail.
46 func loadOrBuildSparseIndex(ctx context.Context, dir, cacheDir string) (sparseIndex, error) {
47 if err := ctx.Err(); err != nil {
48 return sparseIndex{}, err
49 }
50 manifest, err := readStoredManifest(filepath.Join(dir, "manifest.json"))
51 if err != nil {
52 return sparseIndex{}, err
53 }
54 logPath := logPathForManifest(dir, manifest)
55 file, err := os.Open(logPath)
56 if os.IsNotExist(err) {
57 return sparseIndex{Codec: sparseIndexCodec, Entries: []sparseIndexEntry{}}, nil
58 }
59 if err != nil {
60 return sparseIndex{}, err
61 }
62 defer file.Close()
63 info, err := file.Stat()
64 if err != nil {
65 return sparseIndex{}, err
66 }
67 identity, err := sparseLogIdentity(file, info)
68 if err != nil {
69 return sparseIndex{}, err
70 }
71 if data, readErr := os.ReadFile(sparseIndexPath(cacheDir)); readErr == nil {
72 var cached sparseIndex
73 if json.Unmarshal(data, &cached) == nil && cached.validFor(info, identity) {
74 return cached, nil
75 }
76 }
77
78 rebuilt := sparseIndex{
79 Codec: sparseIndexCodec,
80 LogSize: info.Size(),
81 LogModTimeNS: info.ModTime().UnixNano(),
82 LogIdentity: identity,
83 Entries: []sparseIndexEntry{},
84 }
85 commitIndex := 0
86 visit := func(offset int64, commit Commit) bool {
87 if ctx.Err() != nil {
88 return false
89 }
90 if commitIndex%sparseIndexInterval == 0 {
91 rebuilt.Entries = append(rebuilt.Entries, sparseIndexEntry{FirstSequence: commit.FirstSequence, Offset: offset})
92 }
93 commitIndex++
94 rebuilt.CommitCount++
95 rebuilt.LastSequence = commit.LastSequence()
96 return true
97 }
98 if manifest.Codec == Codec {
99 err = scanV4CommitFileRefs(ctx, file, 0, 1, contentStoreForSessionDir(dir), nil, visit)
100 } else {
101 err = scanCommitFileCodec(file, 0, 1, manifest.Codec, nil, visit)
102 }
103 if err != nil {
104 return sparseIndex{}, err
105 }
106 if err := ctx.Err(); err != nil {
107 return sparseIndex{}, err
108 }
109 writeSparseIndex(cacheDir, rebuilt)
110 return rebuilt, nil
111 }
112
113 func (idx sparseIndex) validFor(info os.FileInfo, identity string) bool {
114 if idx.Codec != sparseIndexCodec || idx.LogSize != info.Size() || idx.LogModTimeNS != info.ModTime().UnixNano() || idx.LogIdentity != identity {
115 return false
116 }
117 var previousSequence uint64
118 var previousOffset int64 = -1
119 for i, entry := range idx.Entries {
120 if entry.FirstSequence == 0 || entry.Offset < 0 || entry.Offset >= idx.LogSize || entry.FirstSequence <= previousSequence || entry.Offset <= previousOffset {
121 return false
122 }
123 if i == 0 && (entry.FirstSequence != 1 || entry.Offset != 0) {
124 return false
125 }
126 previousSequence, previousOffset = entry.FirstSequence, entry.Offset
127 }
128 return (idx.LastSequence == 0) == (len(idx.Entries) == 0) && (idx.CommitCount == 0) == (idx.LastSequence == 0)
129 }
130
131 func writeSparseIndex(cacheDir string, index sparseIndex) {
132 data, err := json.Marshal(index)
133 if err != nil || os.MkdirAll(cacheDir, 0o700) != nil {
134 return
135 }
136 _ = fileutil.AtomicWriteFileStrict(sparseIndexPath(cacheDir), append(data, '\n'), 0o600)
137 }
138
139 func (s *Store) recordPersistedIndex(file *os.File, start int64, commits []Commit, lengths []int64) {
140 if s == nil || file == nil || len(commits) == 0 || len(commits) != len(lengths) {
141 return
142 }
143 info, err := file.Stat()
144 if err != nil {
145 return
146 }
147 offset := start
148 for i, commit := range commits {
149 if i == len(commits)-1 {
150 s.tip = durableTip{LogOffset: offset + int64(lengths[i]), AnchorOffset: offset, AnchorFirst: commit.FirstSequence, AnchorCommitID: commit.ID, AnchorHash: commit.OperationHash}
151 }
152 offset += int64(lengths[i])
153 }
154 identity, err := sparseLogIdentity(file, info)
155 if err != nil {
156 return
157 }
158 s.indexMu.Lock()
159 index := s.index
160 if index.partial {
161 index.LogSize = info.Size()
162 index.LogModTimeNS = info.ModTime().UnixNano()
163 index.LogIdentity = identity
164 index.LastSequence = commits[len(commits)-1].LastSequence()
165 s.index = index
166 s.indexMu.Unlock()
167 return
168 }
169 if index.Codec != sparseIndexCodec || index.LogSize != start {
170 s.indexMu.Unlock()
171 _ = s.rebuildWriterIndex(file)
172 return
173 }
174 offset = start
175 for i, commit := range commits {
176 if index.CommitCount%sparseIndexInterval == 0 {
177 index.Entries = append(index.Entries, sparseIndexEntry{FirstSequence: commit.FirstSequence, Offset: offset})
178 }
179 index.CommitCount++
180 index.LastSequence = commit.LastSequence()
181 offset += int64(lengths[i])
182 }
183 index.LogSize = info.Size()
184 index.LogModTimeNS = info.ModTime().UnixNano()
185 index.LogIdentity = identity
186 s.index = index
187 s.indexMu.Unlock()
188 writeSparseIndex(s.dir, index)
189 }
190
191 func (s *Store) rebuildWriterIndex(_ *os.File) error {
192 index, err := loadOrBuildSparseIndex(context.Background(), s.dir, s.dir)
193 if err != nil {
194 return err
195 }
196 s.indexMu.Lock()
197 s.index = index
198 s.indexMu.Unlock()
199 return nil
200 }
201
202 func (idx sparseIndex) checkpoint(offset uint64) sparseIndexEntry {
203 if len(idx.Entries) == 0 {
204 return sparseIndexEntry{FirstSequence: 1}
205 }
206 target := offset + 1
207 position := max(sort.Search(len(idx.Entries), func(i int) bool { return idx.Entries[i].FirstSequence > target })-1, 0)
208 return idx.Entries[position]
209 }
210
211 func sparseLogIdentity(file *os.File, info os.FileInfo) (string, error) {
212 hash := sha256.New()
213 _, _ = fmt.Fprintf(hash, "%d:%d:", info.Size(), info.ModTime().UnixNano())
214 readChunk := func(offset, length int64) error {
215 if length <= 0 {
216 return nil
217 }
218 buf := make([]byte, length)
219 n, err := file.ReadAt(buf, offset)
220 if err != nil && err != io.EOF {
221 return err
222 }
223 _, _ = hash.Write(buf[:n])
224 return nil
225 }
226 first := min(info.Size(), sparseIdentityBytes)
227 if err := readChunk(0, first); err != nil {
228 return "", err
229 }
230 if info.Size() > first {
231 last := min(info.Size()-first, sparseIdentityBytes)
232 if err := readChunk(info.Size()-last, last); err != nil {
233 return "", err
234 }
235 }
236 return hex.EncodeToString(hash.Sum(nil)), nil
237 }
238
238 lines GO