Skip to content

Commit b52a5e8

Browse files
committed
fixes from review
1 parent e3b9fb2 commit b52a5e8

File tree

12 files changed

+162
-68
lines changed

12 files changed

+162
-68
lines changed

cmd/thv-operator/api/v1alpha1/virtualmcpserver_types.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,13 @@ type OperationalConfig struct {
388388
// FailureHandling configures failure handling behavior
389389
// +optional
390390
FailureHandling *FailureHandlingConfig `json:"failureHandling,omitempty"`
391+
392+
// CapabilityCacheTTL is the time-to-live for cached capability entries.
393+
// Defaults to 5 minutes if not specified.
394+
// Shorter values (e.g., "10s") can be used for testing to see capability updates sooner.
395+
// +kubebuilder:default="5m"
396+
// +optional
397+
CapabilityCacheTTL string `json:"capabilityCacheTTL,omitempty"`
391398
}
392399

393400
// TimeoutConfig configures timeout settings

cmd/thv-operator/pkg/vmcpconfig/converter.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -921,5 +921,12 @@ func (*Converter) convertOperational(
921921
}
922922
}
923923

924+
// Parse capability cache TTL
925+
if vmcp.Spec.Operational.CapabilityCacheTTL != "" {
926+
if duration, err := time.ParseDuration(vmcp.Spec.Operational.CapabilityCacheTTL); err == nil {
927+
operational.CapabilityCacheTTL = vmcpconfig.Duration(duration)
928+
}
929+
}
930+
924931
return operational
925932
}

cmd/vmcp/app/commands.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -295,7 +295,16 @@ func runServe(cmd *cobra.Command, _ []string) error {
295295
agg := aggregator.NewDefaultAggregator(backendClient, conflictResolver, cfg.Aggregation.Tools)
296296

297297
// Create discovery manager for lazy per-user capability discovery
298-
discoveryMgr, err := discovery.NewManager(agg)
298+
// Use configured cache TTL if available, otherwise use default
299+
var discoveryMgr discovery.Manager
300+
if cfg.Operational != nil && cfg.Operational.CapabilityCacheTTL > 0 {
301+
cacheTTL := time.Duration(cfg.Operational.CapabilityCacheTTL)
302+
logger.Debugf("Creating discovery manager with custom cache TTL: %v", cacheTTL)
303+
discoveryMgr, err = discovery.NewManagerWithTTL(agg, cacheTTL)
304+
} else {
305+
logger.Debugf("Creating discovery manager with default cache TTL: %v", discovery.DefaultCacheTTL)
306+
discoveryMgr, err = discovery.NewManager(agg)
307+
}
299308
if err != nil {
300309
return fmt.Errorf("failed to create discovery manager: %w", err)
301310
}

deploy/charts/operator-crds/files/crds/toolhive.stacklok.dev_virtualmcpservers.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -644,6 +644,13 @@ spec:
644644
description: Operational defines operational settings like timeouts
645645
and health checks
646646
properties:
647+
capabilityCacheTTL:
648+
default: 5m
649+
description: |-
650+
CapabilityCacheTTL is the time-to-live for cached capability entries.
651+
Defaults to 5 minutes if not specified.
652+
Shorter values (e.g., "10s") can be used for testing to see capability updates sooner.
653+
type: string
647654
failureHandling:
648655
description: FailureHandling configures failure handling behavior
649656
properties:

deploy/charts/operator-crds/templates/toolhive.stacklok.dev_virtualmcpservers.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -647,6 +647,13 @@ spec:
647647
description: Operational defines operational settings like timeouts
648648
and health checks
649649
properties:
650+
capabilityCacheTTL:
651+
default: 5m
652+
description: |-
653+
CapabilityCacheTTL is the time-to-live for cached capability entries.
654+
Defaults to 5 minutes if not specified.
655+
Shorter values (e.g., "10s") can be used for testing to see capability updates sooner.
656+
type: string
650657
failureHandling:
651658
description: FailureHandling configures failure handling behavior
652659
properties:

pkg/vmcp/config/config.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,11 @@ type OperationalConfig struct {
242242

243243
// FailureHandling configures failure handling.
244244
FailureHandling *FailureHandlingConfig `json:"failure_handling,omitempty" yaml:"failure_handling,omitempty"`
245+
246+
// CapabilityCacheTTL is the time-to-live for cached capability entries.
247+
// Defaults to 5 minutes if not specified or zero.
248+
// Shorter values (e.g., 10s) can be used for testing to see capability updates sooner.
249+
CapabilityCacheTTL Duration `json:"capability_cache_ttl,omitempty" yaml:"capability_cache_ttl,omitempty"`
245250
}
246251

247252
// TimeoutConfig configures timeouts.

pkg/vmcp/discovery/health_filter.go

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22
package discovery
33

44
import (
5-
"reflect"
6-
75
"github.com/stacklok/toolhive/pkg/logger"
86
"github.com/stacklok/toolhive/pkg/vmcp"
97
"github.com/stacklok/toolhive/pkg/vmcp/health"
@@ -22,7 +20,7 @@ import (
2220
func FilterHealthyBackends(backends []vmcp.Backend, healthProvider health.StatusProvider) []vmcp.Backend {
2321
// If health monitoring is disabled, return all backends
2422
// Note: Check both interface nil and typed nil (Go interface semantics)
25-
if healthProvider == nil || !isProviderInitialized(healthProvider) {
23+
if !health.IsProviderInitialized(healthProvider) {
2624
return backends
2725
}
2826

@@ -59,10 +57,3 @@ func FilterHealthyBackends(backends []vmcp.Backend, healthProvider health.Status
5957

6058
return filtered
6159
}
62-
63-
// isProviderInitialized checks if the health status provider is properly initialized.
64-
// This handles Go's interface nil semantics where an interface can be "not nil"
65-
// even when its underlying value is nil (e.g., (*health.Monitor)(nil)).
66-
func isProviderInitialized(provider health.StatusProvider) bool {
67-
return provider != nil && !reflect.ValueOf(provider).IsNil()
68-
}

pkg/vmcp/discovery/health_filter_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -341,7 +341,7 @@ func TestIsProviderInitialized(t *testing.T) {
341341
tt := tt
342342
t.Run(tt.name, func(t *testing.T) {
343343
t.Parallel()
344-
result := isProviderInitialized(tt.provider)
344+
result := health.IsProviderInitialized(tt.provider)
345345
assert.Equal(t, tt.expected, result)
346346
})
347347
}

pkg/vmcp/discovery/manager.go

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ import (
2424
//go:generate mockgen -destination=mocks/mock_manager.go -package=mocks -source=manager.go Manager
2525

2626
const (
27-
// cacheTTL is the time-to-live for cached capability entries.
28-
cacheTTL = 5 * time.Minute
27+
// DefaultCacheTTL is the default time-to-live for cached capability entries.
28+
DefaultCacheTTL = 5 * time.Minute
2929
// maxCacheSize is the maximum number of entries allowed in the cache.
3030
maxCacheSize = 1000
3131
// cleanupInterval is how often expired cache entries are removed.
@@ -59,21 +59,33 @@ type cacheEntry struct {
5959
type DefaultManager struct {
6060
aggregator aggregator.Aggregator
6161
cache map[string]*cacheEntry
62+
cacheTTL time.Duration
6263
cacheMu sync.RWMutex
6364
stopCh chan struct{}
6465
stopOnce sync.Once
6566
wg sync.WaitGroup
6667
}
6768

68-
// NewManager creates a new discovery manager with the given aggregator.
69+
// NewManager creates a new discovery manager with the given aggregator using the default cache TTL.
6970
func NewManager(agg aggregator.Aggregator) (Manager, error) {
71+
return NewManagerWithTTL(agg, DefaultCacheTTL)
72+
}
73+
74+
// NewManagerWithTTL creates a new discovery manager with the given aggregator and cache TTL.
75+
// Use this when you need custom cache TTL (e.g., shorter TTL for testing).
76+
func NewManagerWithTTL(agg aggregator.Aggregator, cacheTTL time.Duration) (Manager, error) {
7077
if agg == nil {
7178
return nil, ErrAggregatorNil
7279
}
7380

81+
if cacheTTL <= 0 {
82+
cacheTTL = DefaultCacheTTL
83+
}
84+
7485
m := &DefaultManager{
7586
aggregator: agg,
7687
cache: make(map[string]*cacheEntry),
88+
cacheTTL: cacheTTL,
7789
stopCh: make(chan struct{}),
7890
}
7991

@@ -184,7 +196,7 @@ func (m *DefaultManager) cacheCapabilities(key string, caps *aggregator.Aggregat
184196

185197
m.cache[key] = &cacheEntry{
186198
capabilities: caps,
187-
expiresAt: time.Now().Add(cacheTTL),
199+
expiresAt: time.Now().Add(m.cacheTTL),
188200
}
189201
}
190202

pkg/vmcp/health/monitor.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package health
33
import (
44
"context"
55
"fmt"
6+
"reflect"
67
"sync"
78
"time"
89

@@ -34,6 +35,16 @@ func IsBackendUsable(status vmcp.BackendHealthStatus) bool {
3435
status == vmcp.BackendUnknown
3536
}
3637

38+
// IsProviderInitialized checks if a StatusProvider is properly initialized.
39+
// This handles Go's interface nil semantics where an interface can be "not nil"
40+
// even when its underlying value is nil (e.g., (*Monitor)(nil)).
41+
//
42+
// Returns true if the provider is initialized and can be safely called.
43+
// Returns false if the provider is nil or contains a nil pointer.
44+
func IsProviderInitialized(provider StatusProvider) bool {
45+
return provider != nil && !reflect.ValueOf(provider).IsNil()
46+
}
47+
3748
// healthCheckContextKey is a marker for health check requests.
3849
type healthCheckContextKey struct{}
3950

0 commit comments

Comments
 (0)