| 72 | } |
| 73 | |
| 74 | func NewCachedBridgePool(options PoolOptions, providers []aibridge.Provider, logger slog.Logger, metrics *aibridge.Metrics, tracer trace.Tracer) (*CachedBridgePool, error) { |
| 75 | cache, err := ristretto.NewCache(&ristretto.Config[string, *aibridge.RequestBridge]{ |
| 76 | NumCounters: options.MaxItems * 10, // Docs suggest setting this 10x number of keys. |
| 77 | MaxCost: options.MaxItems * cacheCost, // Up to n instances. |
| 78 | IgnoreInternalCost: true, // Don't try estimate cost using bytes (ristretto does this naïvely anyway, just using the size of the value struct not the REAL memory usage). |
| 79 | BufferItems: 64, // Sticking with recommendation from docs. |
| 80 | Metrics: true, // Collect metrics (only used in tests, for now). |
| 81 | OnEvict: func(item *ristretto.Item[*aibridge.RequestBridge]) { |
| 82 | if item == nil || item.Value == nil { |
| 83 | return |
| 84 | } |
| 85 | // Capture the value synchronously: ristretto reuses the |
| 86 | // item slot after OnEvict returns, so reading item.Value |
| 87 | // from the goroutine below races with the caller of |
| 88 | // Clear/Set. The shutdown still runs in the background to |
| 89 | // avoid blocking ristretto's eviction loop. |
| 90 | bridge := item.Value |
| 91 | go func() { |
| 92 | shutdownCtx, cancel := context.WithTimeout(context.Background(), time.Second*5) |
| 93 | defer cancel() |
| 94 | _ = bridge.Shutdown(shutdownCtx) |
| 95 | }() |
| 96 | }, |
| 97 | }) |
| 98 | if err != nil { |
| 99 | return nil, xerrors.Errorf("create cache: %w", err) |
| 100 | } |
| 101 | |
| 102 | pool := &CachedBridgePool{ |
| 103 | cache: cache, |
| 104 | options: options, |
| 105 | metrics: metrics, |
| 106 | tracer: tracer, |
| 107 | logger: logger, |
| 108 | |
| 109 | singleflight: &singleflight.Group[string, *aibridge.RequestBridge]{}, |
| 110 | |
| 111 | shuttingDownCh: make(chan struct{}), |
| 112 | } |
| 113 | initial := slices.Clone(providers) |
| 114 | pool.providers.Store(&initial) |
| 115 | return pool, nil |
| 116 | } |
| 117 | |
| 118 | // ReplaceProviders swaps the provider snapshot used by future Acquires. |
| 119 | // It is safe to call concurrently with Acquire and is a no-op after |