github.com/grafana/pyroscope@v1.18.0/pkg/metastore/tracing/context_registry.go (about) 1 package tracing 2 3 import ( 4 "context" 5 "sync" 6 "time" 7 8 "github.com/prometheus/client_golang/prometheus" 9 10 "github.com/grafana/pyroscope/pkg/util" 11 ) 12 13 // ContextRegistry maintains a mapping of IDs to contexts for tracing purposes. 14 // This allows us to propagate tracing context from HTTP handlers down to BoltDB transactions 15 // without persisting the context in Raft logs. 16 // 17 // The registry is only used on the leader node where Propose() is called. Followers will 18 // not have contexts available and will use context.Background() instead. 19 type ContextRegistry struct { 20 mu sync.RWMutex 21 entries map[string]*contextEntry 22 // cleanupInterval determines how often we scan for expired entries 23 cleanupInterval time.Duration 24 // entryTTL is the maximum age of an entry before it's considered expired 25 entryTTL time.Duration 26 stop chan struct{} 27 done chan struct{} 28 29 // sizeMetric tracks the number of entries in the registry 30 sizeMetric prometheus.Gauge 31 } 32 33 type contextEntry struct { 34 ctx context.Context 35 created time.Time 36 } 37 38 const ( 39 defaultCleanupInterval = 10 * time.Second 40 defaultEntryTTL = 30 * time.Second 41 ) 42 43 // NewContextRegistry creates a new context registry with background cleanup. 44 func NewContextRegistry(reg prometheus.Registerer) *ContextRegistry { 45 return newContextRegistry(defaultCleanupInterval, defaultEntryTTL, reg) 46 } 47 48 // newContextRegistry creates a new context registry with background cleanup. 49 func newContextRegistry(cleanupInterval, entryTTL time.Duration, reg prometheus.Registerer) *ContextRegistry { 50 if cleanupInterval <= 0 { 51 cleanupInterval = defaultCleanupInterval 52 } 53 if entryTTL <= 0 { 54 entryTTL = defaultEntryTTL 55 } 56 57 sizeMetric := prometheus.NewGauge(prometheus.GaugeOpts{ 58 Name: "context_registry_size", 59 Help: "Number of contexts currently stored in the registry for tracing propagation", 60 }) 61 if reg != nil { 62 util.RegisterOrGet(reg, sizeMetric) 63 } 64 65 r := &ContextRegistry{ 66 entries: make(map[string]*contextEntry), 67 cleanupInterval: cleanupInterval, 68 entryTTL: entryTTL, 69 stop: make(chan struct{}), 70 done: make(chan struct{}), 71 sizeMetric: sizeMetric, 72 } 73 74 go r.cleanupLoop() 75 return r 76 } 77 78 // Store saves a context for the given ID. 79 func (r *ContextRegistry) Store(id string, ctx context.Context) { 80 r.mu.Lock() 81 defer r.mu.Unlock() 82 r.entries[id] = &contextEntry{ 83 ctx: ctx, 84 created: time.Now(), 85 } 86 } 87 88 // Retrieve gets the context for the given ID. 89 func (r *ContextRegistry) Retrieve(id string) (context.Context, bool) { 90 r.mu.RLock() 91 defer r.mu.RUnlock() 92 if entry, ok := r.entries[id]; ok { 93 return entry.ctx, true 94 } 95 return context.Background(), false 96 } 97 98 // Delete removes the context for the given ID. 99 func (r *ContextRegistry) Delete(id string) { 100 r.mu.Lock() 101 defer r.mu.Unlock() 102 delete(r.entries, id) 103 } 104 105 // cleanupLoop periodically removes expired entries from the registry. 106 func (r *ContextRegistry) cleanupLoop() { 107 defer close(r.done) 108 ticker := time.NewTicker(r.cleanupInterval) 109 defer ticker.Stop() 110 111 for { 112 select { 113 case <-ticker.C: 114 r.cleanup() 115 case <-r.stop: 116 return 117 } 118 } 119 } 120 121 // cleanup removes entries that are older than the TTL. 122 func (r *ContextRegistry) cleanup() { 123 r.mu.Lock() 124 defer r.mu.Unlock() 125 126 now := time.Now() 127 for id, entry := range r.entries { 128 if now.Sub(entry.created) > r.entryTTL { 129 delete(r.entries, id) 130 } 131 } 132 133 if r.sizeMetric != nil { 134 r.sizeMetric.Set(float64(len(r.entries))) 135 } 136 } 137 138 // Shutdown stops the cleanup loop. 139 func (r *ContextRegistry) Shutdown() { 140 close(r.stop) 141 <-r.done 142 } 143 144 // Size returns the current number of entries in the registry. 145 // This is primarily useful for metrics and testing. 146 func (r *ContextRegistry) Size() int { 147 r.mu.RLock() 148 defer r.mu.RUnlock() 149 return len(r.entries) 150 }