]> git.lizzy.rs Git - go-anidb.git/blob - cache.go
anidb: (*cacheDir).Get(): reduce the time a file lock is retained
[go-anidb.git] / cache.go
1 package anidb
2
3 import (
4         "bytes"
5         "compress/gzip"
6         "encoding/gob"
7         "errors"
8         "fmt"
9         "io"
10         "log"
11         "os"
12         "path"
13         "reflect"
14         "regexp"
15         "sync"
16         "time"
17 )
18
19 var _ log.Logger
20
21 type Cacheable interface {
22         // Updates the last modified time
23         Touch()
24         // Returns true if the Cacheable is nil, or if the last modified time is too old.
25         IsStale() bool
26 }
27
28 func init() {
29         gob.RegisterName("*github.com/Kovensky/go-anidb.invalidKeyCache", &invalidKeyCache{})
30 }
31
32 type invalidKeyCache struct{ time.Time }
33
34 func (c *invalidKeyCache) Touch() {
35         c.Time = time.Now()
36 }
37 func (c *invalidKeyCache) IsStale() bool {
38         return time.Now().Sub(c.Time) > InvalidKeyCacheDuration
39 }
40
41 type cacheDir struct {
42         *sync.RWMutex
43
44         CacheDir string
45 }
46
47 func init() {
48         if err := SetCacheDir(path.Join(os.TempDir(), "anidb", "cache")); err != nil {
49                 panic(err)
50         }
51 }
52
53 var cache cacheDir
54
55 // Sets the cache directory to the given path.
56 //
57 // go-anidb needs a valid cache directory to function, so, during module
58 // initialization, it uses os.TempDir() to set a default cache dir.
59 // go-anidb panics if it's unable to set the default cache dir.
60 func SetCacheDir(path string) (err error) {
61         m := cache.RWMutex
62         if m == nil {
63                 m = &sync.RWMutex{}
64                 cache.RWMutex = m
65         }
66         cache.Lock()
67
68         if err = os.MkdirAll(path, 0755|os.ModeDir); err != nil {
69                 cache.Unlock()
70                 return err
71         }
72
73         cache = cacheDir{
74                 RWMutex:  m,
75                 CacheDir: path,
76         }
77
78         cache.Unlock()
79         RefreshTitles()
80         return nil
81 }
82
83 // Returns the current cache dir.
84 func GetCacheDir() (path string) {
85         cache.RLock()
86         defer cache.RUnlock()
87
88         return cache.CacheDir
89 }
90
91 type cacheKey interface{}
92
93 // All "bad characters" that can't go in Windows paths.
94 // It's a superset of the "bad characters" on other OSes, so this works.
95 var badPath = regexp.MustCompile(`[\\/:\*\?\"<>\|]`)
96
97 func stringify(stuff ...cacheKey) []string {
98         ret := make([]string, len(stuff))
99         for i := range stuff {
100                 s := fmt.Sprint(stuff[i])
101                 ret[i] = badPath.ReplaceAllLiteralString(s, "_")
102         }
103         return ret
104 }
105
106 // Each key but the last is treated as a directory.
107 // The last key is treated as a regular file.
108 //
109 // This also means that cache keys that are file-backed
110 // cannot have subkeys.
111 func cachePath(keys ...cacheKey) string {
112         parts := append([]string{GetCacheDir()}, stringify(keys...)...)
113         p := path.Join(parts...)
114         return p
115 }
116
117 // Opens the file that backs the specified keys.
118 func (c *cacheDir) Open(keys ...cacheKey) (fh *os.File, err error) {
119         subItem := cachePath(keys...)
120         return os.Open(subItem)
121 }
122
123 // Creates a new file to back the specified keys.
124 func (c *cacheDir) Create(keys ...cacheKey) (fh *os.File, err error) {
125         subItem := cachePath(keys...)
126         subDir := path.Dir(subItem)
127
128         if err = os.MkdirAll(subDir, 0755|os.ModeDir); err != nil {
129                 return nil, err
130         }
131         return os.Create(subItem)
132 }
133
134 // Deletes the file that backs the specified keys.
135 func (c *cacheDir) Delete(keys ...cacheKey) (err error) {
136         return os.Remove(cachePath(keys...))
137 }
138
139 // Deletes the specified key and all subkeys.
140 func (c *cacheDir) DeleteAll(keys ...cacheKey) (err error) {
141         return os.RemoveAll(cachePath(keys...))
142 }
143
144 func (c *cacheDir) Get(v Cacheable, keys ...cacheKey) (err error) {
145         defer func() {
146                 log.Println("Got entry", keys, "(error", err, ")")
147         }()
148
149         val := reflect.ValueOf(v)
150         if k := val.Kind(); k == reflect.Ptr || k == reflect.Interface {
151                 val = val.Elem()
152         }
153         if !val.CanSet() {
154                 // panic because this is an internal coding mistake
155                 panic("(*cacheDir).Get(): given Cacheable is not setable")
156         }
157
158         flock := lockFile(cachePath(keys...))
159         if flock != nil {
160                 flock.Lock()
161         }
162         defer func() {
163                 if flock != nil {
164                         flock.Unlock()
165                 }
166         }()
167
168         fh, err := c.Open(keys...)
169         if err != nil {
170                 return err
171         }
172
173         buf := bytes.Buffer{}
174         if _, err = io.Copy(&buf, fh); err != nil {
175                 fh.Close()
176                 return err
177         }
178         if err = fh.Close(); err != nil {
179                 return err
180         }
181
182         if flock != nil {
183                 flock.Unlock()
184                 flock = nil
185         }
186
187         gz, err := gzip.NewReader(&buf)
188         if err != nil {
189                 return err
190         }
191         defer func() {
192                 if e := gz.Close(); err == nil {
193                         err = e
194                 }
195         }()
196
197         // defer func() {
198         //      if err == io.EOF {
199         //              err = nil
200         //      }
201         // }()
202
203         switch f := gz.Header.Comment; f {
204         case "encoding/gob":
205                 dec := gob.NewDecoder(gz)
206                 err = dec.Decode(v)
207         default:
208                 return errors.New(fmt.Sprintf("Cached data (format %q) is not in a known format", f))
209         }
210
211         return
212 }
213
214 func (c *cacheDir) Set(v Cacheable, keys ...cacheKey) (n int64, err error) {
215         if v := reflect.ValueOf(v); !v.IsValid() {
216                 panic("reflect.ValueOf() returned invaled value")
217         } else if k := v.Kind(); k == reflect.Ptr || k == reflect.Interface {
218                 if v.IsNil() {
219                         return // no point in saving nil
220                 }
221         }
222         defer func() {
223                 log.Println("Set entry", keys, "(error", err, ")")
224         }()
225
226         // First we encode to memory -- we don't want to create/truncate a file and put bad data in it.
227         buf := bytes.Buffer{}
228         gz, err := gzip.NewWriterLevel(&buf, gzip.BestCompression)
229         if err != nil {
230                 return 0, err
231         }
232         gz.Header.Comment = "encoding/gob"
233
234         // it doesn't matter if the caller doesn't see this,
235         // the important part is that the cache does.
236         v.Touch()
237
238         enc := gob.NewEncoder(gz)
239         err = enc.Encode(v)
240
241         if e := gz.Close(); err == nil {
242                 err = e
243         }
244
245         if err != nil {
246                 return 0, err
247         }
248
249         // We have good data, time to actually put it in the cache
250         if flock := lockFile(cachePath(keys...)); flock != nil {
251                 flock.Lock()
252                 defer flock.Unlock()
253         }
254
255         fh, err := c.Create(keys...)
256         if err != nil {
257                 return 0, err
258         }
259         defer func() {
260                 if e := fh.Close(); err == nil {
261                         err = e
262                 }
263         }()
264         n, err = io.Copy(fh, &buf)
265         return
266 }
267
268 // Checks if the given keys are not marked as invalid.
269 //
270 // If the key was marked as invalid but is no longer considered
271 // so, deletes the invalid marker.
272 func (c *cacheDir) CheckValid(keys ...cacheKey) bool {
273         invKeys := append([]cacheKey{"invalid"}, keys...)
274         inv := invalidKeyCache{}
275
276         if cache.Get(&inv, invKeys...) == nil {
277                 if inv.IsStale() {
278                         cache.Delete(invKeys...)
279                 } else {
280                         return false
281                 }
282         }
283         return true
284 }
285
286 // Deletes the given keys and marks them as invalid.
287 //
288 // They are considered invalid for InvalidKeyCacheDuration.
289 func (c *cacheDir) MarkInvalid(keys ...cacheKey) error {
290         invKeys := append([]cacheKey{"invalid"}, keys...)
291
292         cache.Delete(keys...)
293         _, err := cache.Set(&invalidKeyCache{}, invKeys...)
294         return err
295 }