11package symbolizer
22
33import (
4+ "bytes"
45 "context"
56 "errors"
67 "fmt"
78 "io"
89 "net/http"
910 "net/url"
1011 "os"
11- "path/filepath"
1212 "regexp"
1313 "strings"
14+ "sync"
1415 "time"
16+
17+ "github.com/dgraph-io/ristretto"
1518)
1619
1720type DebuginfodClient interface {
18- FetchDebuginfo (ctx context.Context , buildID string ) (string , error )
21+ FetchDebuginfo (ctx context.Context , buildID string ) (io. ReadCloser , error )
1922}
2023
2124type debuginfodClientConfig struct {
@@ -25,10 +28,23 @@ type debuginfodClientConfig struct {
2528 // userAgent string
2629 httpClient * http.Client
2730}
28-
2931type debuginfodClient struct {
3032 cfg debuginfodClientConfig
3133 metrics * Metrics
34+
35+ // In-memory cache of build IDs to file paths
36+ cache * ristretto.Cache
37+
38+ // Track in-flight requests to prevent duplicate fetches
39+ inFlightRequests map [string ]* inFlightRequest
40+ inFlightRequestsMutex sync.Mutex
41+ }
42+
43+ // inFlightRequest represents an ongoing fetch operation
44+ type inFlightRequest struct {
45+ done chan struct {}
46+ data []byte
47+ err error
3248}
3349
3450func NewDebuginfodClient (baseURL string , metrics * Metrics ) DebuginfodClient {
@@ -38,6 +54,16 @@ func NewDebuginfodClient(baseURL string, metrics *Metrics) DebuginfodClient {
3854 TLSHandshakeTimeout : 10 * time .Second ,
3955 }
4056
57+ cache , err := ristretto .NewCache (& ristretto.Config {
58+ NumCounters : 1e7 , // number of keys to track frequency of (10M)
59+ MaxCost : 2 << 30 , // maximum cost of cache (2GB)
60+ BufferItems : 64 , // number of keys per Get buffer
61+ })
62+
63+ if err != nil {
64+ cache = nil
65+ }
66+
4167 return & debuginfodClient {
4268 cfg : debuginfodClientConfig {
4369 baseURL : baseURL ,
@@ -55,12 +81,14 @@ func NewDebuginfodClient(baseURL string, metrics *Metrics) DebuginfodClient {
5581 },
5682 },
5783 },
58- metrics : metrics ,
84+ metrics : metrics ,
85+ cache : cache ,
86+ inFlightRequests : make (map [string ]* inFlightRequest ),
5987 }
6088}
6189
6290// FetchDebuginfo fetches the debuginfo file for a specific build ID.
63- func (c * debuginfodClient ) FetchDebuginfo (ctx context.Context , buildID string ) (string , error ) {
91+ func (c * debuginfodClient ) FetchDebuginfo (ctx context.Context , buildID string ) (io. ReadCloser , error ) {
6492 start := time .Now ()
6593 var lastErr error
6694 c .metrics .debuginfodRequestsTotal .Inc ()
@@ -76,23 +104,67 @@ func (c *debuginfodClient) FetchDebuginfo(ctx context.Context, buildID string) (
76104 sanitizedBuildID , err := sanitizeBuildID (buildID )
77105 if err != nil {
78106 c .metrics .debuginfodRequestErrorsTotal .WithLabelValues ("invalid_id" ).Inc ()
79- return "" , err
107+ return nil , err
108+ }
109+
110+ // Check in-memory cache first
111+ if c .cache != nil {
112+ if data , found := c .cache .Get (sanitizedBuildID ); found {
113+ return io .NopCloser (bytes .NewReader (data .([]byte ))), nil
114+ }
80115 }
81116
117+ // Check if there's already a request in flight for this build ID
118+ c .inFlightRequestsMutex .Lock ()
119+ req , inFlight := c .inFlightRequests [sanitizedBuildID ]
120+ if inFlight {
121+ // There's already a request in flight, wait for it to complete
122+ done := req .done
123+ c .inFlightRequestsMutex .Unlock ()
124+
125+ select {
126+ case <- done :
127+ if req .err != nil {
128+ lastErr = req .err
129+ return nil , req .err
130+ }
131+ return io .NopCloser (bytes .NewReader (req .data )), nil
132+ case <- ctx .Done ():
133+ lastErr = ctx .Err ()
134+ return nil , ctx .Err ()
135+ }
136+ }
137+
138+ // Create a new in-flight request
139+ req = & inFlightRequest {
140+ done : make (chan struct {}),
141+ }
142+ c .inFlightRequests [sanitizedBuildID ] = req
143+ c .inFlightRequestsMutex .Unlock ()
144+
145+ // Ensure we clean up the in-flight request when we're done
146+ defer func () {
147+ c .inFlightRequestsMutex .Lock ()
148+ delete (c .inFlightRequests , sanitizedBuildID )
149+ close (req .done )
150+ c .inFlightRequestsMutex .Unlock ()
151+ }()
152+
82153 url := fmt .Sprintf ("%s/buildid/%s/debuginfo" , c .cfg .baseURL , sanitizedBuildID )
154+ var data []byte
83155
84156 // Implement retries with exponential backoff
85157 for attempt := 0 ; attempt < c .cfg .maxRetries ; attempt ++ {
86158 if attempt > 0 {
87159 if ctx .Err () != nil {
88- return "" , ctx .Err ()
160+ return nil , ctx .Err ()
89161 }
90162 time .Sleep (c .cfg .backoffTime * time .Duration (attempt ))
91163 }
92164
93- filePath , err : = c .doRequest (ctx , url , sanitizedBuildID )
165+ data , err = c .doRequest (ctx , url )
94166 if err == nil {
95- return filePath , nil
167+ break
96168 }
97169
98170 lastErr = err
@@ -101,50 +173,52 @@ func (c *debuginfodClient) FetchDebuginfo(ctx context.Context, buildID string) (
101173 }
102174 }
103175
104- return "" , fmt .Errorf ("failed to fetch debuginfo after %d attempts: %w" , c .cfg .maxRetries , lastErr )
176+ if lastErr != nil {
177+ req .err = fmt .Errorf ("failed to fetch debuginfo after %d attempts: %w" , c .cfg .maxRetries , lastErr )
178+ return nil , req .err
179+ }
180+
181+ // Store in cache
182+ if c .cache != nil {
183+ // The cost is the size of the data in bytes
184+ c .cache .Set (sanitizedBuildID , data , int64 (len (data )))
185+ }
186+
187+ req .data = data
188+ return io .NopCloser (bytes .NewReader (data )), nil
105189}
106190
107- func (c * debuginfodClient ) doRequest (ctx context.Context , url , buildID string ) (string , error ) {
191+ func (c * debuginfodClient ) doRequest (ctx context.Context , url string ) ([] byte , error ) {
108192 req , err := http .NewRequestWithContext (ctx , http .MethodGet , url , nil )
109193 if err != nil {
110- return "" , fmt .Errorf ("failed to create request: %w" , err )
194+ return nil , fmt .Errorf ("failed to create request: %w" , err )
111195 }
112196
113197 req .Header .Set ("Accept-Encoding" , "gzip, deflate" )
114198
115199 resp , err := c .cfg .httpClient .Do (req )
116200 if err != nil {
117- return "" , fmt .Errorf ("failed to execute request: %w" , err )
201+ return nil , fmt .Errorf ("failed to execute request: %w" , err )
118202 }
119203 defer resp .Body .Close ()
120204
121205 if resp .StatusCode != http .StatusOK {
122- return "" , fmt .Errorf ("unexpected HTTP status: %d %s" , resp .StatusCode , resp .Status )
123- }
124-
125- return c .saveDebugInfo (resp .Body , buildID , resp .ContentLength )
126- }
127-
128- func (c * debuginfodClient ) saveDebugInfo (body io.Reader , buildID string , contentLength int64 ) (string , error ) {
129- if contentLength > 0 {
130- c .metrics .debuginfodFileSize .Observe (float64 (contentLength ))
206+ return nil , fmt .Errorf ("unexpected HTTP status: %d %s" , resp .StatusCode , resp .Status )
131207 }
132208
133- tempDir := os .TempDir ()
134- filePath := filepath .Join (tempDir , fmt .Sprintf ("%s.elf" , buildID ))
135-
136- outFile , err := os .Create (filePath )
209+ // Read the entire response body into memory
210+ data , err := io .ReadAll (resp .Body )
137211 if err != nil {
138- return "" , fmt .Errorf ("failed to create temp file : %w" , err )
212+ return nil , fmt .Errorf ("failed to read response body : %w" , err )
139213 }
140- defer outFile .Close ()
141214
142- if _ , err := io .Copy (outFile , body ); err != nil {
143- os .Remove (filePath ) // Clean up on error
144- return "" , fmt .Errorf ("failed to write debug info: %w" , err )
215+ if resp .ContentLength > 0 {
216+ c .metrics .debuginfodFileSize .Observe (float64 (resp .ContentLength ))
217+ } else {
218+ c .metrics .debuginfodFileSize .Observe (float64 (len (data )))
145219 }
146220
147- return filePath , nil
221+ return data , nil
148222}
149223
150224func isRetryableError (err error ) bool {
0 commit comments