repo_cache.go

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