Skip to content

Commit 7a05cec

Browse files
committed
cache package
1 parent 991806d commit 7a05cec

File tree

2 files changed

+445
-0
lines changed

2 files changed

+445
-0
lines changed

extensions/cache/cache.go

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License. See LICENSE in the project root for license information.
3+
4+
package cache
5+
6+
import (
7+
"context"
8+
"errors"
9+
"os"
10+
"path/filepath"
11+
"sync"
12+
"time"
13+
14+
"github.com/AzureAD/microsoft-authentication-extensions-for-go/extensions/accessor"
15+
"github.com/AzureAD/microsoft-authentication-extensions-for-go/extensions/internal/lock"
16+
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/cache"
17+
)
18+
19+
var (
20+
// retryDelay lets tests prevent delays when faking errors in Replace
21+
retryDelay = 10 * time.Millisecond
22+
// timeout lets tests set the default amount of time allowed to read from the accessor
23+
timeout = time.Second
24+
)
25+
26+
// locker helps tests fake Lock
27+
type locker interface {
28+
Lock(context.Context) error
29+
Unlock() error
30+
}
31+
32+
// Cache caches authentication data in external storage, using a file lock to coordinate
33+
// access to it with other processes.
34+
type Cache struct {
35+
// a provides read/write access to storage
36+
a accessor.Accessor
37+
// data is accessor's data as of the last sync
38+
data []byte
39+
// l coordinates with other processes
40+
l locker
41+
// m coordinates this process's goroutines
42+
m *sync.Mutex
43+
// sync is when this Cache last read from or wrote to a
44+
sync time.Time
45+
// ts is the path to a file used to timestamp Export and Replace operations
46+
ts string
47+
}
48+
49+
// New is the constructor for Cache. "p" is the path to a file used to track when stored
50+
// data changes. Export will create this file and any directories in its path which don't
51+
// already exist.
52+
func New(a accessor.Accessor, p string) (*Cache, error) {
53+
lock, err := lock.New(p+".lockfile", retryDelay)
54+
if err != nil {
55+
return nil, err
56+
}
57+
return &Cache{a: a, l: lock, m: &sync.Mutex{}, ts: p}, err
58+
}
59+
60+
// Export writes the bytes marshaled by "m" to the accessor.
61+
// MSAL clients call this method automatically.
62+
func (c *Cache) Export(ctx context.Context, m cache.Marshaler, h cache.ExportHints) (err error) {
63+
c.m.Lock()
64+
defer c.m.Unlock()
65+
66+
data, err := m.Marshal()
67+
if err != nil {
68+
return err
69+
}
70+
err = c.l.Lock(ctx)
71+
if err != nil {
72+
return err
73+
}
74+
defer func() {
75+
e := c.l.Unlock()
76+
if err == nil {
77+
err = e
78+
}
79+
}()
80+
if err = c.a.Write(ctx, data); err == nil {
81+
// touch the timestamp file to record the time of this write; discard any
82+
// error because this is just an optimization to avoid redundant reads
83+
c.sync = time.Now()
84+
if er := os.Chtimes(c.ts, c.sync, c.sync); errors.Is(er, os.ErrNotExist) {
85+
if er = os.MkdirAll(filepath.Dir(c.ts), 0700); er == nil {
86+
f, _ := os.OpenFile(c.ts, os.O_CREATE, 0600)
87+
_ = f.Close()
88+
}
89+
}
90+
c.data = data
91+
}
92+
return err
93+
}
94+
95+
// Replace reads bytes from the accessor and unmarshals them to "u".
96+
// MSAL clients call this method automatically.
97+
func (c *Cache) Replace(ctx context.Context, u cache.Unmarshaler, h cache.ReplaceHints) error {
98+
c.m.Lock()
99+
defer c.m.Unlock()
100+
101+
// If the timestamp file indicates cached data hasn't changed since we last read or wrote it,
102+
// return c.data, which is the data as of that time. Discard any error from reading the timestamp
103+
// because this is just an optimization to prevent unnecessary reads. If we don't know whether
104+
// cached data has changed, we assume it has.
105+
read := true
106+
data := c.data
107+
f, err := os.Stat(c.ts)
108+
if err == nil {
109+
mt := f.ModTime()
110+
read = !mt.Equal(c.sync)
111+
}
112+
if _, hasDeadline := ctx.Deadline(); !hasDeadline {
113+
var cancel context.CancelFunc
114+
ctx, cancel = context.WithTimeout(ctx, timeout)
115+
defer cancel()
116+
}
117+
// Unmarshal the accessor's data, reading it first if needed. We don't acquire the file lock before
118+
// reading from the accessor because it isn't strictly necessary and is relatively expensive. In the
119+
// unlikely event that a read overlaps with a write and returns malformed data, Unmarshal will return
120+
// an error and we'll try another read.
121+
for {
122+
if read {
123+
data, err = c.a.Read(ctx)
124+
if err != nil {
125+
break
126+
}
127+
}
128+
err = u.Unmarshal(data)
129+
if err == nil {
130+
break
131+
} else if !read {
132+
// c.data is apparently corrupt; Read from the accessor before trying again
133+
read = true
134+
}
135+
select {
136+
case <-ctx.Done():
137+
return ctx.Err()
138+
case <-time.After(retryDelay):
139+
// Unmarshal error; try again
140+
}
141+
}
142+
// Update the sync time only if we read from the accessor and unmarshaled its data. Otherwise
143+
// the data hasn't changed since the last read/write, or reading failed and we'll try again on
144+
// the next call.
145+
if err == nil && read {
146+
c.data = data
147+
if f, err := os.Stat(c.ts); err == nil {
148+
c.sync = f.ModTime()
149+
}
150+
}
151+
return err
152+
}
153+
154+
var _ cache.ExportReplace = (*Cache)(nil)

0 commit comments

Comments
 (0)