Skip to content

Commit 41e655c

Browse files
feat: Improve faro.receiver.sourcemaps caching strategy (#4337)
Co-authored-by: Clayton Cornell <131809008+clayton-cornell@users.noreply.github.com>
1 parent d45ccc0 commit 41e655c

File tree

5 files changed

+368
-23
lines changed

5 files changed

+368
-23
lines changed

docs/sources/reference/components/faro/faro.receiver.md

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,11 +61,13 @@ You can use the following blocks with `faro.receiver`:
6161
| [`server`][server] | Configures the HTTP server. | no |
6262
| `server` > [`rate_limiting`][rate_limiting] | Configures rate limiting for the HTTP server. | no |
6363
| [`sourcemaps`][sourcemaps] | Configures sourcemap retrieval. | no |
64+
| `sourcemaps` > [`cache`][cache] | Configures sourcemap caching behavior. | no |
6465
| `sourcemaps` > [`location`][location] | Configures on-disk location for sourcemap retrieval. | no |
6566

6667
The > symbol indicates deeper levels of nesting.
6768
For example, `sourcemaps` > `location` refers to a `location` block defined inside a `sourcemaps` block.
6869

70+
[cache]: #cache
6971
[location]: #location
7072
[output]: #output
7173
[rate_limiting]: #rate_limiting
@@ -149,7 +151,7 @@ The `sourcemaps` block configures how to retrieve sourcemaps.
149151
Sourcemaps are then used to transform file and line information from minified code into the file and line information from the original source code.
150152

151153
| Name | Type | Description | Default | Required |
152-
|-------------------------|----------------|--------------------------------------------|---------|----------|
154+
| ----------------------- | -------------- | ------------------------------------------ | ------- | -------- |
153155
| `download` | `bool` | Whether to download sourcemaps. | `true` | no |
154156
| `download_from_origins` | `list(string)` | Which origins to download sourcemaps from. | `["*"]` | no |
155157
| `download_timeout` | `duration` | Timeout when downloading sourcemaps. | `"1s"` | no |
@@ -168,6 +170,25 @@ Setting `download_timeout` to `"0s"` disables timeouts.
168170
To retrieve sourcemaps from disk instead of the network, specify one or more [`location` blocks][location].
169171
When `location` blocks are provided, they're checked first for sourcemaps before falling back to downloading.
170172

173+
#### `cache`
174+
175+
The `cache` block configures sourcemap caching behavior.
176+
177+
| Name | Type | Description | Default | Required |
178+
| ------------------------ | ---------- | ----------------------------------------------------------------------------------------- | ------- | -------- |
179+
| `cleanup_check_interval` | `duration` | How often {{< param "PRODUCT_NAME" >}} checks cached sourcemaps for cleanup. | `"30s"` | no |
180+
| `error_cleanup_interval` | `duration` | How long {{< param "PRODUCT_NAME" >}} waits before retrying a failed source map download. | `"1h"` | no |
181+
| `ttl` | `duration` | How long {{< param "PRODUCT_NAME" >}} keeps an unused source map in the cache. | `inf` | no |
182+
183+
By default, {{< param "PRODUCT_NAME" >}} keeps sourcemaps in memory indefinitely.
184+
Set `ttl` to remove sourcemaps that are not accessed within the specified duration.
185+
186+
{{< param "PRODUCT_NAME" >}} caches errors that occur while downloading or parsing a sourcemap.
187+
Use `error_cleanup_interval` to control how long these errors remain cached.
188+
189+
Cached sourcemaps are checked for cleanup every 30 seconds by default.
190+
Set `cleanup_check_interval` to adjust this frequency.
191+
171192
#### `location`
172193

173194
The `location` block declares a location where sourcemaps are stored on the filesystem.
@@ -223,7 +244,7 @@ The template value is replaced with the release value provided by the [Faro Web
223244
* `faro_receiver_request_message_bytes` (histogram): Size (in bytes) of HTTP requests received from clients.
224245
* `faro_receiver_response_message_bytes` (histogram): Size (in bytes) of HTTP responses sent to clients.
225246
* `faro_receiver_inflight_requests` (gauge): Current number of inflight requests.
226-
* `faro_receiver_sourcemap_cache_size` (counter): Number of items in sourcemap cache per origin.
247+
* `faro_receiver_sourcemap_cache_size` (gauge): Number of items in sourcemap cache per origin.
227248
* `faro_receiver_sourcemap_downloads_total` (counter): Total number of sourcemap downloads performed per origin and status.
228249
* `faro_receiver_sourcemap_file_reads_total` (counter): Total number of sourcemap retrievals using the filesystem per origin and status.
229250
* `faro_receiver_rate_limiter_active_app` (gauge): Number of active applications with rate limiters. Inactive limiters are cleaned up every 10 minutes.

internal/component/faro/receiver/arguments.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package receiver
33
import (
44
"encoding"
55
"fmt"
6+
"math"
67
"time"
78

89
"github.com/alecthomas/units"
@@ -76,6 +77,7 @@ type SourceMapsArguments struct {
7677
Download bool `alloy:"download,attr,optional"`
7778
DownloadFromOrigins []string `alloy:"download_from_origins,attr,optional"`
7879
DownloadTimeout time.Duration `alloy:"download_timeout,attr,optional"`
80+
Cache *CacheArguments `alloy:"cache,block,optional"`
7981
Locations []LocationArguments `alloy:"location,block,optional"`
8082
}
8183

@@ -84,6 +86,23 @@ func (s *SourceMapsArguments) SetToDefault() {
8486
Download: true,
8587
DownloadFromOrigins: []string{"*"},
8688
DownloadTimeout: time.Second,
89+
Cache: &CacheArguments{},
90+
}
91+
s.Cache.SetToDefault()
92+
}
93+
94+
// CacheArguments configures sourcemap caching behavior.
95+
type CacheArguments struct {
96+
TTL time.Duration `alloy:"ttl,attr,optional"`
97+
ErrorCleanupInterval time.Duration `alloy:"error_cleanup_interval,attr,optional"`
98+
CleanupCheckInterval time.Duration `alloy:"cleanup_check_interval,attr,optional"`
99+
}
100+
101+
func (c *CacheArguments) SetToDefault() {
102+
*c = CacheArguments{
103+
TTL: time.Duration(math.MaxInt64),
104+
ErrorCleanupInterval: time.Hour,
105+
CleanupCheckInterval: time.Second * 30,
87106
}
88107
}
89108

internal/component/faro/receiver/receiver.go

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,13 +131,20 @@ func (c *Component) Update(args component.Arguments) error {
131131

132132
c.handler.Update(newArgs.Server)
133133

134-
c.lazySourceMaps.SetInner(newSourceMapsStore(
134+
// Stop old store's cleanup if there is one
135+
c.lazySourceMaps.Stop()
136+
137+
innerStore := newSourceMapsStore(
135138
log.With(c.log, "subcomponent", "handler"),
136139
newArgs.SourceMaps,
137140
c.sourceMapsMetrics,
138141
nil, // Use default HTTP client.
139142
nil, // Use default FS implementation.
140-
))
143+
)
144+
c.lazySourceMaps.SetInner(innerStore)
145+
146+
// Start cleanup for new store
147+
c.lazySourceMaps.Start()
141148

142149
c.logs.SetReceivers(newArgs.Output.Logs)
143150
c.traces.SetConsumers(newArgs.Output.Traces)
@@ -243,3 +250,21 @@ func (vs *varSourceMapsStore) SetInner(inner sourceMapsStore) {
243250

244251
vs.inner = inner
245252
}
253+
254+
func (vs *varSourceMapsStore) Start() {
255+
vs.mut.RLock()
256+
defer vs.mut.RUnlock()
257+
258+
if vs.inner != nil {
259+
vs.inner.Start()
260+
}
261+
}
262+
263+
func (vs *varSourceMapsStore) Stop() {
264+
vs.mut.RLock()
265+
defer vs.mut.RUnlock()
266+
267+
if vs.inner != nil {
268+
vs.inner.Stop()
269+
}
270+
}

internal/component/faro/receiver/sourcemaps.go

Lines changed: 146 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package receiver
22

33
import (
44
"bytes"
5+
"context"
56
"fmt"
67
"io"
78
"io/fs"
@@ -13,6 +14,7 @@ import (
1314
"strings"
1415
"sync"
1516
"text/template"
17+
"time"
1618

1719
"github.com/go-kit/log"
1820
"github.com/go-sourcemap/sourcemap"
@@ -28,6 +30,8 @@ import (
2830
// transforming minified source locations to the original source location.
2931
type sourceMapsStore interface {
3032
GetSourceMap(sourceURL string, release string) (*sourcemap.Consumer, error)
33+
Start()
34+
Stop()
3135
}
3236

3337
// Stub interfaces for easier mocking.
@@ -67,14 +71,14 @@ func (fs osFileService) ReadFile(name string) ([]byte, error) {
6771
}
6872

6973
type sourceMapMetrics struct {
70-
cacheSize *prometheus.CounterVec
74+
cacheSize *prometheus.GaugeVec
7175
downloads *prometheus.CounterVec
7276
fileReads *prometheus.CounterVec
7377
}
7478

7579
func newSourceMapMetrics(reg prometheus.Registerer) *sourceMapMetrics {
7680
m := &sourceMapMetrics{
77-
cacheSize: prometheus.NewCounterVec(prometheus.CounterOpts{
81+
cacheSize: prometheus.NewGaugeVec(prometheus.GaugeOpts{
7882
Name: "faro_receiver_sourcemap_cache_size",
7983
Help: "number of items in source map cache, per origin",
8084
}, []string{"origin"}),
@@ -88,7 +92,7 @@ func newSourceMapMetrics(reg prometheus.Registerer) *sourceMapMetrics {
8892
}, []string{"origin", "status"}),
8993
}
9094

91-
m.cacheSize = util.MustRegisterOrGet(reg, m.cacheSize).(*prometheus.CounterVec)
95+
m.cacheSize = util.MustRegisterOrGet(reg, m.cacheSize).(*prometheus.GaugeVec)
9296
m.downloads = util.MustRegisterOrGet(reg, m.downloads).(*prometheus.CounterVec)
9397
m.fileReads = util.MustRegisterOrGet(reg, m.fileReads).(*prometheus.CounterVec)
9498
return m
@@ -99,6 +103,16 @@ type sourcemapFileLocation struct {
99103
pathTemplate *template.Template
100104
}
101105

106+
type timeSource interface {
107+
Now() time.Time
108+
}
109+
110+
type realTimeSource struct{}
111+
112+
func (realTimeSource) Now() time.Time {
113+
return time.Now()
114+
}
115+
102116
type sourceMapsStoreImpl struct {
103117
log log.Logger
104118
cli httpClient
@@ -107,8 +121,18 @@ type sourceMapsStoreImpl struct {
107121
metrics *sourceMapMetrics
108122
locs []*sourcemapFileLocation
109123

110-
cacheMut sync.Mutex
111-
cache map[string]*sourcemap.Consumer
124+
cacheMut sync.Mutex
125+
cache map[string]*cachedSourceMap
126+
timeSource timeSource
127+
cleanupCtx context.Context
128+
cleanupCancel context.CancelFunc
129+
cleanupWg sync.WaitGroup
130+
isStarted bool
131+
}
132+
133+
type cachedSourceMap struct {
134+
consumer *sourcemap.Consumer
135+
lastUsed time.Time
112136
}
113137

114138
// newSourceMapStore creates an implementation of sourceMapsStore. The returned
@@ -141,27 +165,28 @@ func newSourceMapsStore(log log.Logger, args SourceMapsArguments, metrics *sourc
141165
}
142166

143167
return &sourceMapsStoreImpl{
144-
log: log,
145-
cli: cli,
146-
fs: fs,
147-
args: args,
148-
cache: make(map[string]*sourcemap.Consumer),
149-
metrics: metrics,
150-
locs: locs,
168+
log: log,
169+
cli: cli,
170+
fs: fs,
171+
args: args,
172+
cache: make(map[string]*cachedSourceMap),
173+
metrics: metrics,
174+
locs: locs,
175+
timeSource: realTimeSource{},
151176
}
152177
}
153178

154179
func (store *sourceMapsStoreImpl) GetSourceMap(sourceURL string, release string) (*sourcemap.Consumer, error) {
155-
// TODO(rfratto): GetSourceMap is weak to transient errors, since it always
156-
// caches the result, even when there's an error. This means that transient
157-
// errors will be cached forever, preventing source maps from being retrieved.
158-
159180
store.cacheMut.Lock()
160181
defer store.cacheMut.Unlock()
161182

162183
cacheKey := fmt.Sprintf("%s__%s", sourceURL, release)
163-
if sm, ok := store.cache[cacheKey]; ok {
164-
return sm, nil
184+
if cached, ok := store.cache[cacheKey]; ok {
185+
if cached != nil {
186+
cached.lastUsed = store.timeSource.Now()
187+
return cached.consumer, nil
188+
}
189+
return nil, nil
165190
}
166191

167192
content, sourceMapURL, err := store.getSourceMapContent(sourceURL, release)
@@ -177,11 +202,113 @@ func (store *sourceMapsStoreImpl) GetSourceMap(sourceURL string, release string)
177202
return nil, err
178203
}
179204
level.Info(store.log).Log("msg", "successfully parsed source map", "url", sourceMapURL, "release", release)
180-
store.cache[cacheKey] = consumer
205+
store.cache[cacheKey] = &cachedSourceMap{
206+
consumer: consumer,
207+
lastUsed: store.timeSource.Now(),
208+
}
181209
store.metrics.cacheSize.WithLabelValues(getOrigin(sourceURL)).Inc()
182210
return consumer, nil
183211
}
184212

213+
func (store *sourceMapsStoreImpl) CleanOldCacheEntries() {
214+
store.cacheMut.Lock()
215+
defer store.cacheMut.Unlock()
216+
217+
ttl := store.args.Cache.TTL
218+
for key, cached := range store.cache {
219+
if cached != nil && cached.lastUsed.Before(store.timeSource.Now().Add(-ttl)) {
220+
srcUrl := strings.SplitN(key, "__", 2)[0]
221+
origin := getOrigin(srcUrl)
222+
store.metrics.cacheSize.WithLabelValues(origin).Dec()
223+
delete(store.cache, key)
224+
}
225+
}
226+
}
227+
228+
func (store *sourceMapsStoreImpl) CleanCachedErrors() {
229+
store.cacheMut.Lock()
230+
defer store.cacheMut.Unlock()
231+
232+
for key, cached := range store.cache {
233+
if cached == nil {
234+
delete(store.cache, key)
235+
}
236+
}
237+
}
238+
239+
// Start begins the cleanup routines based on configured cache intervals.
240+
func (store *sourceMapsStoreImpl) Start() {
241+
store.cacheMut.Lock()
242+
defer store.cacheMut.Unlock()
243+
244+
if store.isStarted {
245+
return
246+
}
247+
store.isStarted = true
248+
249+
cacheConfig := store.args.Cache
250+
if cacheConfig == nil {
251+
return
252+
}
253+
254+
store.cleanupCtx, store.cleanupCancel = context.WithCancel(context.Background())
255+
256+
if d := cacheConfig.CleanupCheckInterval; d > 0 {
257+
store.cleanupWg.Add(1)
258+
go func(interval time.Duration) {
259+
defer store.cleanupWg.Done()
260+
store.CleanOldCacheEntries()
261+
ticker := time.NewTicker(interval)
262+
defer ticker.Stop()
263+
for {
264+
select {
265+
case <-store.cleanupCtx.Done():
266+
return
267+
case <-ticker.C:
268+
store.CleanOldCacheEntries()
269+
}
270+
}
271+
}(d)
272+
}
273+
274+
if d := cacheConfig.ErrorCleanupInterval; d > 0 {
275+
store.cleanupWg.Add(1)
276+
go func(interval time.Duration) {
277+
defer store.cleanupWg.Done()
278+
store.CleanCachedErrors()
279+
ticker := time.NewTicker(interval)
280+
defer ticker.Stop()
281+
for {
282+
select {
283+
case <-store.cleanupCtx.Done():
284+
return
285+
case <-ticker.C:
286+
store.CleanCachedErrors()
287+
}
288+
}
289+
}(d)
290+
}
291+
}
292+
293+
// Stop terminates all cleanup goroutines and waits for them to finish.
294+
func (store *sourceMapsStoreImpl) Stop() {
295+
store.cacheMut.Lock()
296+
defer store.cacheMut.Unlock()
297+
298+
if !store.isStarted {
299+
return
300+
}
301+
store.isStarted = false
302+
303+
if store.cleanupCancel != nil {
304+
store.cleanupCancel()
305+
store.cleanupCancel = nil
306+
}
307+
308+
store.cleanupWg.Wait()
309+
store.cleanupCtx = nil
310+
}
311+
185312
func (store *sourceMapsStoreImpl) getSourceMapContent(sourceURL string, release string) (content []byte, sourceMapURL string, err error) {
186313
// Attempt to find the source map in the filesystem first.
187314
for _, loc := range store.locs {

0 commit comments

Comments
 (0)