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