Acquire retrieves or creates a [*aibridge.RequestBridge] instance per given key. Each returned [*aibridge.RequestBridge] is safe for concurrent use. Each [*aibridge.RequestBridge] is stateful because it has MCP clients which maintain sessions to the configured MCP server.
(ctx context.Context, req Request, clientFn ClientFunc, mcpProxyFactory MCPProxyBuilder)
| 153 | // Each returned [*aibridge.RequestBridge] is safe for concurrent use. |
| 154 | // Each [*aibridge.RequestBridge] is stateful because it has MCP clients which maintain sessions to the configured MCP server. |
| 155 | func (p *CachedBridgePool) Acquire(ctx context.Context, req Request, clientFn ClientFunc, mcpProxyFactory MCPProxyBuilder) (_ http.Handler, outErr error) { |
| 156 | spanAttrs := []attribute.KeyValue{ |
| 157 | attribute.String(tracing.InitiatorID, req.InitiatorID.String()), |
| 158 | attribute.String(tracing.APIKeyID, req.APIKeyID), |
| 159 | } |
| 160 | ctx, span := p.tracer.Start(ctx, "CachedBridgePool.Acquire", trace.WithAttributes(spanAttrs...)) |
| 161 | defer tracing.EndSpanErr(span, &outErr) |
| 162 | ctx = tracing.WithRequestBridgeAttributesInContext(ctx, spanAttrs) |
| 163 | |
| 164 | if err := ctx.Err(); err != nil { |
| 165 | return nil, xerrors.Errorf("acquire: %w", err) |
| 166 | } |
| 167 | |
| 168 | select { |
| 169 | case <-p.shuttingDownCh: |
| 170 | return nil, xerrors.New("pool shutting down") |
| 171 | default: |
| 172 | } |
| 173 | |
| 174 | // Wait for all buffered writes to be applied, otherwise multiple calls in quick succession |
| 175 | // may visit the slow path unnecessarily. |
| 176 | defer p.cache.Wait() |
| 177 | |
| 178 | // Fast path. |
| 179 | cacheKey := req.InitiatorID.String() + "|" + req.APIKeyID |
| 180 | bridge, ok := p.cache.Get(cacheKey) |
| 181 | if ok && bridge != nil { |
| 182 | // TODO: future improvement: |
| 183 | // Once we can detect token expiry against an MCP server, we no longer need to let these instances |
| 184 | // expire after the original TTL; we can extend the TTL on each Acquire() call. |
| 185 | // For now, we need to let the instance expiry to keep the MCP connections fresh. |
| 186 | |
| 187 | span.AddEvent("cache_hit") |
| 188 | return bridge, nil |
| 189 | } |
| 190 | |
| 191 | span.AddEvent("cache_miss") |
| 192 | providerVersion := p.providerVersion.Load() |
| 193 | recorder := aibridge.NewRecorder(p.logger.Named("recorder"), p.tracer, func() (aibridge.Recorder, error) { |
| 194 | client, err := clientFn() |
| 195 | if err != nil { |
| 196 | return nil, xerrors.Errorf("acquire client: %w", err) |
| 197 | } |
| 198 | |
| 199 | return &recorderTranslation{apiKeyID: req.APIKeyID, client: client}, nil |
| 200 | }) |
| 201 | |
| 202 | // Slow path. |
| 203 | // Creating an *aibridge.RequestBridge may take some time, so gate all subsequent callers behind the initial request and return the resulting value. |
| 204 | // TODO: track startup time since it adds latency to first request (histogram count will also help us see how often this occurs). |
| 205 | singleflightKey := cacheKey + "|" + strconv.FormatInt(providerVersion, 10) |
| 206 | instance, err, _ := p.singleflight.Do(singleflightKey, func() (*aibridge.RequestBridge, error) { |
| 207 | var ( |
| 208 | mcpServers mcp.ServerProxier |
| 209 | err error |
| 210 | ) |
| 211 | |
| 212 | mcpServers, err = mcpProxyFactory.Build(ctx, req, p.tracer) |