1package cache
2
3import (
4 "bytes"
5 "encoding/gob"
6 "fmt"
7 "sync"
8
9 "github.com/pkg/errors"
10
11 "github.com/MichaelMure/git-bug/entities/bug"
12 "github.com/MichaelMure/git-bug/entities/identity"
13 "github.com/MichaelMure/git-bug/entity"
14 "github.com/MichaelMure/git-bug/repository"
15)
16
17type Excerpt interface {
18 Id() entity.Id
19}
20
21type CacheEntity interface {
22 NeedCommit() bool
23}
24
25type getUserIdentityFunc func() (identity.Interface, error)
26
27type SubCache[ExcerptT Excerpt, CacheT CacheEntity, EntityT entity.Interface] struct {
28 repo repository.ClockedRepo
29 resolvers entity.Resolvers
30
31 getUserIdentity getUserIdentityFunc
32 readWithResolver func(repository.ClockedRepo, entity.Resolvers, entity.Id) (EntityT, error)
33 makeCached func(*SubCache[ExcerptT, CacheT, EntityT], getUserIdentityFunc, EntityT) CacheT
34 makeExcerpt func() Excerpt
35
36 typename string
37 namespace string
38 version uint
39 maxLoaded int
40
41 mu sync.RWMutex
42 excerpts map[entity.Id]ExcerptT
43 cached map[entity.Id]CacheT
44 lru *lruIdCache
45}
46
47func NewSubCache[ExcerptT Excerpt, CacheT CacheEntity, EntityT entity.Interface](
48 repo repository.ClockedRepo,
49 resolvers entity.Resolvers,
50 getUserIdentity func() (identity.Interface, error),
51 typename, namespace string,
52 version uint, maxLoaded int) *SubCache[ExcerptT, CacheT, EntityT] {
53 return &SubCache[ExcerptT, CacheT, EntityT]{
54 repo: repo,
55 resolvers: resolvers,
56 getUserIdentity: getUserIdentity,
57 typename: typename,
58 namespace: namespace,
59 version: version,
60 maxLoaded: maxLoaded,
61 excerpts: make(map[entity.Id]ExcerptT),
62 cached: make(map[entity.Id]CacheT),
63 lru: newLRUIdCache(),
64 }
65}
66
67// Load will try to read from the disk the entity cache file
68func (sc *SubCache[ExcerptT, CacheT, EntityT]) Load() error {
69 sc.mu.Lock()
70 defer sc.mu.Unlock()
71
72 f, err := sc.repo.LocalStorage().Open(sc.namespace + "-file")
73 if err != nil {
74 return err
75 }
76
77 decoder := gob.NewDecoder(f)
78
79 aux := struct {
80 Version uint
81 Excerpts map[entity.Id]ExcerptT
82 }{}
83
84 err = decoder.Decode(&aux)
85 if err != nil {
86 return err
87 }
88
89 if aux.Version != sc.version {
90 return fmt.Errorf("unknown %s cache format version %v", sc.namespace, aux.Version)
91 }
92
93 sc.excerpts = aux.Excerpts
94
95 index, err := sc.repo.GetBleveIndex("bug")
96 if err != nil {
97 return err
98 }
99
100 // simple heuristic to detect a mismatch between the index and the entities
101 count, err := index.DocCount()
102 if err != nil {
103 return err
104 }
105 if count != uint64(len(sc.excerpts)) {
106 return fmt.Errorf("count mismatch between bleve and %s excerpts", sc.namespace)
107 }
108
109 return nil
110}
111
112// Write will serialize on disk the entity cache file
113func (sc *SubCache[ExcerptT, CacheT, EntityT]) Write() error {
114 sc.mu.RLock()
115 defer sc.mu.RUnlock()
116
117 var data bytes.Buffer
118
119 aux := struct {
120 Version uint
121 Excerpts map[entity.Id]ExcerptT
122 }{
123 Version: sc.version,
124 Excerpts: sc.excerpts,
125 }
126
127 encoder := gob.NewEncoder(&data)
128
129 err := encoder.Encode(aux)
130 if err != nil {
131 return err
132 }
133
134 f, err := sc.repo.LocalStorage().Create(sc.namespace + "-file")
135 if err != nil {
136 return err
137 }
138
139 _, err = f.Write(data.Bytes())
140 if err != nil {
141 return err
142 }
143
144 return f.Close()
145}
146
147func (sc *SubCache[ExcerptT, CacheT, EntityT]) Build() {
148
149}
150
151// AllIds return all known bug ids
152func (sc *SubCache[ExcerptT, CacheT, EntityT]) AllIds() []entity.Id {
153 sc.mu.RLock()
154 defer sc.mu.RUnlock()
155
156 result := make([]entity.Id, len(sc.excerpts))
157
158 i := 0
159 for _, excerpt := range sc.excerpts {
160 result[i] = excerpt.Id()
161 i++
162 }
163
164 return result
165}
166
167// Resolve retrieve an entity matching the exact given id
168func (sc *SubCache[ExcerptT, CacheT, EntityT]) Resolve(id entity.Id) (CacheT, error) {
169 sc.mu.RLock()
170 cached, ok := sc.cached[id]
171 if ok {
172 sc.lru.Get(id)
173 sc.mu.RUnlock()
174 return cached, nil
175 }
176 sc.mu.RUnlock()
177
178 b, err := sc.readWithResolver(sc.repo, sc.resolvers, id)
179 if err != nil {
180 return nil, err
181 }
182
183 cached = sc.makeCached(sc, sc.getUserIdentity, b)
184
185 sc.mu.Lock()
186 sc.cached[id] = cached
187 sc.lru.Add(id)
188 sc.mu.Unlock()
189
190 sc.evictIfNeeded()
191
192 return cached, nil
193}
194
195// ResolvePrefix retrieve an entity matching an id prefix. It fails if multiple
196// entity match.
197func (sc *SubCache[ExcerptT, CacheT, EntityT]) ResolvePrefix(prefix string) (CacheT, error) {
198 return sc.ResolveMatcher(func(excerpt ExcerptT) bool {
199 return excerpt.Id().HasPrefix(prefix)
200 })
201}
202
203func (sc *SubCache[ExcerptT, CacheT, EntityT]) ResolveMatcher(f func(ExcerptT) bool) (CacheT, error) {
204 id, err := sc.resolveMatcher(f)
205 if err != nil {
206 return nil, err
207 }
208 return sc.Resolve(id)
209}
210
211// ResolveExcerpt retrieve an Excerpt matching the exact given id
212func (sc *SubCache[ExcerptT, CacheT, EntityT]) ResolveExcerpt(id entity.Id) (ExcerptT, error) {
213 sc.mu.RLock()
214 defer sc.mu.RUnlock()
215
216 excerpt, ok := sc.excerpts[id]
217 if !ok {
218 return nil, entity.NewErrNotFound(sc.typename)
219 }
220
221 return excerpt, nil
222}
223
224// ResolveExcerptPrefix retrieve an Excerpt matching an id prefix. It fails if multiple
225// entity match.
226func (sc *SubCache[ExcerptT, CacheT, EntityT]) ResolveExcerptPrefix(prefix string) (ExcerptT, error) {
227 return sc.ResolveExcerptMatcher(func(excerpt ExcerptT) bool {
228 return excerpt.Id().HasPrefix(prefix)
229 })
230}
231
232func (sc *SubCache[ExcerptT, CacheT, EntityT]) ResolveExcerptMatcher(f func(ExcerptT) bool) (ExcerptT, error) {
233 id, err := sc.resolveMatcher(f)
234 if err != nil {
235 return nil, err
236 }
237 return sc.ResolveExcerpt(id)
238}
239
240func (sc *SubCache[ExcerptT, CacheT, EntityT]) resolveMatcher(f func(ExcerptT) bool) (entity.Id, error) {
241 sc.mu.RLock()
242 defer sc.mu.RUnlock()
243
244 // preallocate but empty
245 matching := make([]entity.Id, 0, 5)
246
247 for _, excerpt := range sc.excerpts {
248 if f(excerpt) {
249 matching = append(matching, excerpt.Id())
250 }
251 }
252
253 if len(matching) > 1 {
254 return entity.UnsetId, entity.NewErrMultipleMatch(sc.typename, matching)
255 }
256
257 if len(matching) == 0 {
258 return entity.UnsetId, entity.NewErrNotFound(sc.typename)
259 }
260
261 return matching[0], nil
262}
263
264var errNotInCache = errors.New("entity missing from cache")
265
266func (sc *SubCache[ExcerptT, CacheT, EntityT]) add(e EntityT) (CacheT, error) {
267 sc.mu.Lock()
268 if _, has := sc.cached[e.Id()]; has {
269 sc.mu.Unlock()
270 return nil, fmt.Errorf("entity %s already exist in the cache", e.Id())
271 }
272
273 cached := sc.makeCached(sc, sc.getUserIdentity, e)
274 sc.cached[e.Id()] = cached
275 sc.lru.Add(e.Id())
276 sc.mu.Unlock()
277
278 sc.evictIfNeeded()
279
280 // force the write of the excerpt
281 err := sc.entityUpdated(e.Id())
282 if err != nil {
283 return nil, err
284 }
285
286 return cached, nil
287}
288
289func (sc *SubCache[ExcerptT, CacheT, EntityT]) Remove(prefix string) error {
290 e, err := sc.ResolvePrefix(prefix)
291 if err != nil {
292 return err
293 }
294
295 sc.mu.Lock()
296
297 err = bug.Remove(c.repo, b.Id())
298 if err != nil {
299 c.muBug.Unlock()
300
301 return err
302 }
303
304 delete(c.bugs, b.Id())
305 delete(c.bugExcerpts, b.Id())
306 c.loadedBugs.Remove(b.Id())
307
308 c.muBug.Unlock()
309
310 return c.writeBugCache()
311}
312
313// entityUpdated is a callback to trigger when the excerpt of an entity changed
314func (sc *SubCache[ExcerptT, CacheT, EntityT]) entityUpdated(id entity.Id) error {
315 sc.mu.Lock()
316 b, ok := sc.cached[id]
317 if !ok {
318 sc.mu.Unlock()
319
320 // if the bug is not loaded at this point, it means it was loaded before
321 // but got evicted. Which means we potentially have multiple copies in
322 // memory and thus concurrent write.
323 // Failing immediately here is the simple and safe solution to avoid
324 // complicated data loss.
325 return errNotInCache
326 }
327 sc.lru.Get(id)
328 // sc.excerpts[id] = bug2.NewBugExcerpt(b.bug, b.Snapshot())
329 sc.excerpts[id] = bug2.NewBugExcerpt(b.bug, b.Snapshot())
330 sc.mu.Unlock()
331
332 if err := sc.addBugToSearchIndex(b.Snapshot()); err != nil {
333 return err
334 }
335
336 // we only need to write the bug cache
337 return sc.Write()
338}
339
340// evictIfNeeded will evict an entity from the cache if needed
341func (sc *SubCache[ExcerptT, CacheT, EntityT]) evictIfNeeded() {
342 sc.mu.Lock()
343 defer sc.mu.Unlock()
344 if sc.lru.Len() <= sc.maxLoaded {
345 return
346 }
347
348 for _, id := range sc.lru.GetOldestToNewest() {
349 b := sc.cached[id]
350 if b.NeedCommit() {
351 continue
352 }
353
354 b.Lock()
355 sc.lru.Remove(id)
356 delete(sc.cached, id)
357
358 if sc.lru.Len() <= sc.maxLoaded {
359 return
360 }
361 }
362}