subcache.go

  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}