Skip to content

Commit 359fb12

Browse files
committed
implement context aware instance manager selection with new TTL manager
1 parent a27d0bf commit 359fb12

File tree

12 files changed

+684
-26
lines changed

12 files changed

+684
-26
lines changed

backend/app/instance_provider.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,13 @@ import (
1313
// InstanceFactoryFunc factory method for creating app instances.
1414
type InstanceFactoryFunc func(ctx context.Context, settings backend.AppInstanceSettings) (instancemgmt.Instance, error)
1515

16-
// NewInstanceManager creates a new app instance manager,
16+
// NewInstanceManager creates a new app instance manager.
1717
//
1818
// This is a helper method for calling NewInstanceProvider and creating a new instancemgmt.InstanceProvider,
19-
// and providing that to instancemgmt.New.
19+
// and providing that to instancemgmt.NewInstanceManagerWrapper.
2020
func NewInstanceManager(fn InstanceFactoryFunc) instancemgmt.InstanceManager {
2121
ip := NewInstanceProvider(fn)
22-
return instancemgmt.New(ip)
22+
return instancemgmt.NewInstanceManagerWrapper(ip)
2323
}
2424

2525
// NewInstanceProvider create a new app instance provider,

backend/datasource/instance_provider.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ type InstanceFactoryFunc func(ctx context.Context, settings backend.DataSourceIn
3030
// and providing that to instancemgmt.New.
3131
func NewInstanceManager(fn InstanceFactoryFunc) instancemgmt.InstanceManager {
3232
ip := NewInstanceProvider(fn)
33-
return instancemgmt.New(ip)
33+
return instancemgmt.NewInstanceManagerWrapper(ip)
3434
}
3535

3636
// NewInstanceProvider create a new data source instance provuder,
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package instancemgmt
2+
3+
import (
4+
"context"
5+
6+
"github.com/grafana/grafana-plugin-sdk-go/backend"
7+
"github.com/grafana/grafana-plugin-sdk-go/experimental/featuretoggles"
8+
)
9+
10+
// NewInstanceManagerWrapper creates a new instance manager that dynamically selects
11+
// between standard and TTL instance managers based on feature toggles from the Grafana config.
12+
func NewInstanceManagerWrapper(provider InstanceProvider) InstanceManager {
13+
return &instanceManagerWrapper{
14+
provider: provider,
15+
standardManager: New(provider),
16+
ttlManager: NewTTLInstanceManager(provider),
17+
}
18+
}
19+
20+
// instanceManagerWrapper is a wrapper that dynamically selects the appropriate
21+
// instance manager implementation based on feature toggles in the context.
22+
type instanceManagerWrapper struct {
23+
provider InstanceProvider
24+
standardManager InstanceManager
25+
ttlManager InstanceManager
26+
}
27+
28+
// selectManager returns the appropriate instance manager based on the feature toggle
29+
// from the Grafana config in the plugin context.
30+
func (c *instanceManagerWrapper) selectManager(_ context.Context, pluginContext backend.PluginContext) InstanceManager {
31+
// Check if TTL instance manager feature toggle is enabled
32+
if pluginContext.GrafanaConfig != nil {
33+
featureToggles := pluginContext.GrafanaConfig.FeatureToggles()
34+
if featureToggles.IsEnabled(featuretoggles.TTLInstanceManager) {
35+
return c.ttlManager
36+
}
37+
}
38+
39+
// Default to standard instance manager
40+
return c.standardManager
41+
}
42+
43+
// Get returns an Instance using the appropriate manager based on feature toggles.
44+
func (c *instanceManagerWrapper) Get(ctx context.Context, pluginContext backend.PluginContext) (Instance, error) {
45+
manager := c.selectManager(ctx, pluginContext)
46+
return manager.Get(ctx, pluginContext)
47+
}
48+
49+
// Do provides an Instance as argument to fn using the appropriate manager based on feature toggles.
50+
func (c *instanceManagerWrapper) Do(ctx context.Context, pluginContext backend.PluginContext, fn InstanceCallbackFunc) error {
51+
manager := c.selectManager(ctx, pluginContext)
52+
return manager.Do(ctx, pluginContext, fn)
53+
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
package instancemgmt
2+
3+
import (
4+
"context"
5+
"testing"
6+
"time"
7+
8+
"github.com/stretchr/testify/require"
9+
10+
"github.com/grafana/grafana-plugin-sdk-go/backend"
11+
"github.com/grafana/grafana-plugin-sdk-go/experimental/featuretoggles"
12+
)
13+
14+
func TestInstanceManagerWrapper(t *testing.T) {
15+
ctx := context.Background()
16+
tip := &testInstanceProvider{}
17+
im := NewInstanceManagerWrapper(tip)
18+
19+
t.Run("Should use standard manager when feature toggle is disabled", func(t *testing.T) {
20+
pCtx := backend.PluginContext{
21+
OrgID: 1,
22+
AppInstanceSettings: &backend.AppInstanceSettings{
23+
Updated: time.Now(),
24+
},
25+
GrafanaConfig: backend.NewGrafanaCfg(map[string]string{
26+
featuretoggles.EnabledFeatures: "",
27+
}),
28+
}
29+
30+
manager := im.(*instanceManagerWrapper).selectManager(ctx, pCtx)
31+
require.IsType(t, &instanceManager{}, manager)
32+
})
33+
34+
t.Run("Should use TTL manager when feature toggle is enabled", func(t *testing.T) {
35+
pCtx := backend.PluginContext{
36+
OrgID: 1,
37+
AppInstanceSettings: &backend.AppInstanceSettings{
38+
Updated: time.Now(),
39+
},
40+
GrafanaConfig: backend.NewGrafanaCfg(map[string]string{
41+
featuretoggles.EnabledFeatures: featuretoggles.TTLInstanceManager,
42+
}),
43+
}
44+
45+
manager := im.(*instanceManagerWrapper).selectManager(ctx, pCtx)
46+
require.IsType(t, &instanceManagerWithTTL{}, manager)
47+
})
48+
49+
t.Run("Should use standard manager when GrafanaConfig is nil", func(t *testing.T) {
50+
pCtx := backend.PluginContext{
51+
OrgID: 1,
52+
AppInstanceSettings: &backend.AppInstanceSettings{
53+
Updated: time.Now(),
54+
},
55+
GrafanaConfig: nil,
56+
}
57+
58+
manager := im.(*instanceManagerWrapper).selectManager(ctx, pCtx)
59+
require.IsType(t, &instanceManager{}, manager)
60+
})
61+
62+
t.Run("Should use TTL manager when feature toggle is enabled with other flags", func(t *testing.T) {
63+
pCtx := backend.PluginContext{
64+
OrgID: 1,
65+
AppInstanceSettings: &backend.AppInstanceSettings{
66+
Updated: time.Now(),
67+
},
68+
GrafanaConfig: backend.NewGrafanaCfg(map[string]string{
69+
featuretoggles.EnabledFeatures: "someOtherFlag," + featuretoggles.TTLInstanceManager + ",anotherFlag",
70+
}),
71+
}
72+
73+
manager := im.(*instanceManagerWrapper).selectManager(ctx, pCtx)
74+
require.IsType(t, &instanceManagerWithTTL{}, manager)
75+
})
76+
77+
t.Run("Should delegate Get calls correctly", func(t *testing.T) {
78+
// Test with TTL manager enabled
79+
pCtx := backend.PluginContext{
80+
OrgID: 1,
81+
AppInstanceSettings: &backend.AppInstanceSettings{
82+
Updated: time.Now(),
83+
},
84+
GrafanaConfig: backend.NewGrafanaCfg(map[string]string{
85+
featuretoggles.EnabledFeatures: featuretoggles.TTLInstanceManager,
86+
}),
87+
}
88+
89+
instance, err := im.Get(ctx, pCtx)
90+
require.NoError(t, err)
91+
require.NotNil(t, instance)
92+
require.Equal(t, pCtx.OrgID, instance.(*testInstance).orgID)
93+
})
94+
95+
t.Run("Should delegate Do calls correctly", func(t *testing.T) {
96+
// Test with standard manager (no feature toggle)
97+
pCtx := backend.PluginContext{
98+
OrgID: 2,
99+
AppInstanceSettings: &backend.AppInstanceSettings{
100+
Updated: time.Now(),
101+
},
102+
GrafanaConfig: backend.NewGrafanaCfg(map[string]string{
103+
featuretoggles.EnabledFeatures: "",
104+
}),
105+
}
106+
107+
var receivedInstance *testInstance
108+
err := im.Do(ctx, pCtx, func(instance *testInstance) {
109+
receivedInstance = instance
110+
})
111+
require.NoError(t, err)
112+
require.NotNil(t, receivedInstance)
113+
require.Equal(t, pCtx.OrgID, receivedInstance.orgID)
114+
})
115+
}

backend/instancemgmt/doc.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,19 @@
11
// Package instancemgmt provides utilities for managing plugin instances.
2+
//
3+
// This package offers several instance manager implementations:
4+
//
5+
// 1. Standard Instance Manager (New): Uses sync.Map for caching instances
6+
// and disposes them when they need updates.
7+
//
8+
// 2. TTL Instance Manager (NewTTLInstanceManager): Uses TTL-based caching
9+
// that automatically evicts instances after a configurable time period.
10+
//
11+
// 3. Instance Manager Wrapper (NewInstanceManagerWrapper):
12+
// Dynamically selects between standard and TTL managers based on
13+
// feature toggles from the Grafana config in the context.
14+
//
15+
// The context-aware manager checks the "ttlInstanceManager" feature toggle
16+
// from the Grafana configuration and automatically uses the appropriate
17+
// underlying implementation. This allows runtime switching without requiring
18+
// plugin restarts or static configuration.
219
package instancemgmt

backend/instancemgmt/instance_manager.go

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,10 @@ var (
1818
Name: "active_instances",
1919
Help: "The number of active plugin instances",
2020
})
21-
disposeTTL = 5 * time.Second
2221
)
2322

23+
const defaultDisposeTTL = 5 * time.Second // Time to wait before disposing an instance
24+
2425
// Instance is a marker interface for an instance.
2526
type Instance interface{}
2627

@@ -73,21 +74,33 @@ type InstanceProvider interface {
7374

7475
// New create a new instance manager.
7576
func New(provider InstanceProvider) InstanceManager {
77+
return newInstanceManager(provider, defaultDisposeTTL)
78+
}
79+
80+
func newInstanceManager(provider InstanceProvider, disposeTTL time.Duration) *instanceManager {
7681
if provider == nil {
7782
panic("provider cannot be nil")
7883
}
7984

85+
if disposeTTL <= 0 {
86+
disposeTTL = defaultDisposeTTL
87+
}
88+
8089
return &instanceManager{
81-
provider: provider,
82-
cache: sync.Map{},
83-
locker: newLocker(),
90+
provider: provider,
91+
cache: sync.Map{},
92+
locker: newLocker(),
93+
disposeTTL: disposeTTL,
8494
}
8595
}
8696

8797
type instanceManager struct {
88-
locker *locker
89-
provider InstanceProvider
90-
cache sync.Map
98+
locker *locker
99+
provider InstanceProvider
100+
cache sync.Map
101+
disposeTTL time.Duration
102+
103+
disposeMutex sync.Mutex // Mutex to protect disposeTTL access in tests
91104
}
92105

93106
func (im *instanceManager) Get(ctx context.Context, pluginContext backend.PluginContext) (Instance, error) {
@@ -121,7 +134,7 @@ func (im *instanceManager) Get(ctx context.Context, pluginContext backend.Plugin
121134
}
122135

123136
if disposer, valid := ci.instance.(InstanceDisposer); valid {
124-
time.AfterFunc(disposeTTL, func() {
137+
time.AfterFunc(im.disposeTTL, func() {
125138
disposer.Dispose()
126139
activeInstances.Dec()
127140
})
@@ -157,6 +170,14 @@ func (im *instanceManager) Do(ctx context.Context, pluginContext backend.PluginC
157170
return nil
158171
}
159172

173+
// setDisposeTTL sets the TTL for disposing instances.
174+
// This method is only used for testing purposes to control the dispose timing behavior.
175+
func (im *instanceManager) setDisposeTTL(ttl time.Duration) {
176+
im.disposeMutex.Lock()
177+
defer im.disposeMutex.Unlock()
178+
im.disposeTTL = ttl
179+
}
180+
160181
func callInstanceHandlerFunc(fn InstanceCallbackFunc, instance interface{}) {
161182
var params = []reflect.Value{}
162183
params = append(params, reflect.ValueOf(instance))

backend/instancemgmt/instance_manager_test.go

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@ import (
77
"testing"
88
"time"
99

10-
"github.com/grafana/grafana-plugin-sdk-go/backend"
1110
"github.com/stretchr/testify/require"
11+
12+
"github.com/grafana/grafana-plugin-sdk-go/backend"
1213
)
1314

1415
func TestInstanceManager(t *testing.T) {
@@ -21,7 +22,7 @@ func TestInstanceManager(t *testing.T) {
2122
}
2223

2324
tip := &testInstanceProvider{}
24-
im := New(tip)
25+
im := newInstanceManager(tip, time.Millisecond)
2526

2627
t.Run("When getting instance should create a new instance", func(t *testing.T) {
2728
instance, err := im.Get(ctx, pCtx)
@@ -43,11 +44,6 @@ func TestInstanceManager(t *testing.T) {
4344
Updated: time.Now(),
4445
},
4546
}
46-
origDisposeTTL := disposeTTL
47-
disposeTTL = time.Millisecond
48-
t.Cleanup(func() {
49-
disposeTTL = origDisposeTTL
50-
})
5147
newInstance, err := im.Get(ctx, pCtxUpdated)
5248

5349
t.Run("New instance should be created", func(t *testing.T) {
@@ -110,12 +106,6 @@ func TestInstanceManagerConcurrency(t *testing.T) {
110106
})
111107

112108
t.Run("Check possible race condition issues when re-creating instance on settings update", func(t *testing.T) {
113-
origDisposeTTL := disposeTTL
114-
disposeTTL = time.Millisecond
115-
t.Cleanup(func() {
116-
disposeTTL = origDisposeTTL
117-
})
118-
119109
ctx := context.Background()
120110
initialPCtx := backend.PluginContext{
121111
OrgID: 1,
@@ -124,7 +114,8 @@ func TestInstanceManagerConcurrency(t *testing.T) {
124114
},
125115
}
126116
tip := &testInstanceProvider{}
127-
im := New(tip)
117+
im := New(tip).(*instanceManager)
118+
im.setDisposeTTL(time.Millisecond)
128119
// Creating initial instance with old contexts
129120
instanceToDispose, _ := im.Get(ctx, initialPCtx)
130121

0 commit comments

Comments
 (0)