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  }