repo_cache.go

  1package cache
  2
  3import (
  4	"fmt"
  5	"io"
  6	"io/ioutil"
  7	"os"
  8	"strconv"
  9
 10	"golang.org/x/sync/errgroup"
 11
 12	"github.com/MichaelMure/git-bug/entities/bug"
 13	"github.com/MichaelMure/git-bug/entities/identity"
 14	"github.com/MichaelMure/git-bug/entity"
 15	"github.com/MichaelMure/git-bug/repository"
 16	"github.com/MichaelMure/git-bug/util/process"
 17)
 18
 19// 1: original format
 20// 2: added cache for identities with a reference in the bug cache
 21// 3: no more legacy identity
 22// 4: entities make their IDs from data, not git commit
 23const formatVersion = 4
 24
 25// The maximum number of bugs loaded in memory. After that, eviction will be done.
 26const defaultMaxLoadedBugs = 1000
 27
 28var _ repository.RepoCommon = &RepoCache{}
 29var _ repository.RepoConfig = &RepoCache{}
 30var _ repository.RepoKeyring = &RepoCache{}
 31
 32// RepoCache is a cache for a Repository. This cache has multiple functions:
 33//
 34// 1. After being loaded, a Bug is kept in memory in the cache, allowing for fast
 35// 		access later.
 36// 2. The cache maintain in memory and on disk a pre-digested excerpt for each bug,
 37// 		allowing for fast querying the whole set of bugs without having to load
 38//		them individually.
 39// 3. The cache guarantee that a single instance of a Bug is loaded at once, avoiding
 40// 		loss of data that we could have with multiple copies in the same process.
 41// 4. The same way, the cache maintain in memory a single copy of the loaded identities.
 42//
 43// The cache also protect the on-disk data by locking the git repository for its
 44// own usage, by writing a lock file. Of course, normal git operations are not
 45// affected, only git-bug related one.
 46type RepoCache struct {
 47	// the underlying repo
 48	repo repository.ClockedRepo
 49
 50	// the name of the repository, as defined in the MultiRepoCache
 51	name string
 52
 53	// resolvers for all known entities
 54	resolvers entity.Resolvers
 55
 56	bugs       *RepoCacheBug
 57	identities *RepoCacheIdentity
 58
 59	subcaches []cacheMgmt
 60
 61	// the user identity's id, if known
 62	userIdentityId entity.Id
 63}
 64
 65func NewRepoCache(r repository.ClockedRepo) (*RepoCache, error) {
 66	return NewNamedRepoCache(r, "")
 67}
 68
 69func NewNamedRepoCache(r repository.ClockedRepo, name string) (*RepoCache, error) {
 70	c := &RepoCache{
 71		repo: r,
 72		name: name,
 73	}
 74
 75	bugs := NewSubCache[*BugExcerpt, *BugCache, bug.Interface](r,
 76		c.getResolvers, c.GetUserIdentity,
 77		"bug", "bugs",
 78		formatVersion, defaultMaxLoadedBugs)
 79
 80	c.subcaches = append(c.subcaches, bugs)
 81	c.bugs = &RepoCacheBug{SubCache: *bugs}
 82
 83	c.resolvers = entity.Resolvers{
 84		&IdentityCache{}: entity.ResolverFunc((func(id entity.Id) (entity.Interface, error)(c.identities.Resolve)),
 85		&BugCache{}:      c.bugs,
 86	}
 87
 88	err := c.lock()
 89	if err != nil {
 90		return &RepoCache{}, err
 91	}
 92
 93	err = c.load()
 94	if err == nil {
 95		return c, nil
 96	}
 97
 98	// Cache is either missing, broken or outdated. Rebuilding.
 99	err = c.buildCache()
100	if err != nil {
101		return nil, err
102	}
103
104	return c, c.write()
105}
106
107// Bugs gives access to the Bug entities
108func (c *RepoCache) Bugs() *RepoCacheBug {
109	return c.bugs
110}
111
112// Identities gives access to the Identity entities
113func (c *RepoCache) Identities() *RepoCacheIdentity {
114	return c.identities
115}
116
117func (c *RepoCache) getResolvers() entity.Resolvers {
118	return c.resolvers
119}
120
121// setCacheSize change the maximum number of loaded bugs
122func (c *RepoCache) setCacheSize(size int) {
123	c.maxLoadedBugs = size
124	c.evictIfNeeded()
125}
126
127// load will try to read from the disk all the cache files
128func (c *RepoCache) load() error {
129	var errG errgroup.Group
130	for _, mgmt := range c.subcaches {
131		errG.Go(mgmt.Load)
132	}
133	return errG.Wait()
134}
135
136// write will serialize on disk all the cache files
137func (c *RepoCache) write() error {
138	var errG errgroup.Group
139	for _, mgmt := range c.subcaches {
140		errG.Go(mgmt.Write)
141	}
142	return errG.Wait()
143}
144
145func (c *RepoCache) lock() error {
146	err := repoIsAvailable(c.repo)
147	if err != nil {
148		return err
149	}
150
151	f, err := c.repo.LocalStorage().Create(lockfile)
152	if err != nil {
153		return err
154	}
155
156	pid := fmt.Sprintf("%d", os.Getpid())
157	_, err = f.Write([]byte(pid))
158	if err != nil {
159		return err
160	}
161
162	return f.Close()
163}
164
165func (c *RepoCache) Close() error {
166	var errG errgroup.Group
167	for _, mgmt := range c.subcaches {
168		errG.Go(mgmt.Close)
169	}
170	err := errG.Wait()
171	if err != nil {
172		return err
173	}
174
175	err = c.repo.Close()
176	if err != nil {
177		return err
178	}
179
180	return c.repo.LocalStorage().Remove(lockfile)
181}
182
183func (c *RepoCache) buildCache() error {
184	_, _ = fmt.Fprintf(os.Stderr, "Building identity cache... ")
185
186	c.identitiesExcerpts = make(map[entity.Id]*IdentityExcerpt)
187
188	allIdentities := identity.ReadAllLocal(c.repo)
189
190	for i := range allIdentities {
191		if i.Err != nil {
192			return i.Err
193		}
194
195		c.identitiesExcerpts[i.Identity.Id()] = NewIdentityExcerpt(i.Identity)
196	}
197
198	_, _ = fmt.Fprintln(os.Stderr, "Done.")
199
200	_, _ = fmt.Fprintf(os.Stderr, "Building bug cache... ")
201
202	c.bugExcerpts = make(map[entity.Id]*BugExcerpt)
203
204	allBugs := bug.ReadAllWithResolver(c.repo, c.resolvers)
205
206	// wipe the index just to be sure
207	err := c.repo.ClearBleveIndex("bug")
208	if err != nil {
209		return err
210	}
211
212	for b := range allBugs {
213		if b.Err != nil {
214			return b.Err
215		}
216
217		snap := b.Bug.Compile()
218		c.bugExcerpts[b.Bug.Id()] = NewBugExcerpt(b.Bug, snap)
219
220		if err := c.addBugToSearchIndex(snap); err != nil {
221			return err
222		}
223	}
224
225	_, _ = fmt.Fprintln(os.Stderr, "Done.")
226
227	return nil
228}
229
230// repoIsAvailable check is the given repository is locked by a Cache.
231// Note: this is a smart function that will clean the lock file if the
232// corresponding process is not there anymore.
233// If no error is returned, the repo is free to edit.
234func repoIsAvailable(repo repository.RepoStorage) error {
235	// Todo: this leave way for a racey access to the repo between the test
236	// if the file exist and the actual write. It's probably not a problem in
237	// practice because using a repository will be done from user interaction
238	// or in a context where a single instance of git-bug is already guaranteed
239	// (say, a server with the web UI running). But still, that might be nice to
240	// have a mutex or something to guard that.
241
242	// Todo: this will fail if somehow the filesystem is shared with another
243	// computer. Should add a configuration that prevent the cleaning of the
244	// lock file
245
246	f, err := repo.LocalStorage().Open(lockfile)
247	if err != nil && !os.IsNotExist(err) {
248		return err
249	}
250
251	if err == nil {
252		// lock file already exist
253		buf, err := ioutil.ReadAll(io.LimitReader(f, 10))
254		if err != nil {
255			return err
256		}
257		if len(buf) == 10 {
258			return fmt.Errorf("the lock file should be < 10 bytes")
259		}
260
261		pid, err := strconv.Atoi(string(buf))
262		if err != nil {
263			return err
264		}
265
266		if process.IsRunning(pid) {
267			return fmt.Errorf("the repository you want to access is already locked by the process pid %d", pid)
268		}
269
270		// The lock file is just laying there after a crash, clean it
271
272		fmt.Println("A lock file is present but the corresponding process is not, removing it.")
273		err = f.Close()
274		if err != nil {
275			return err
276		}
277
278		err = repo.LocalStorage().Remove(lockfile)
279		if err != nil {
280			return err
281		}
282	}
283
284	return nil
285}