Repository: grafana/grafana-plugin-sdk-go (go)

Issue:
Make "instance" a core SDK idea
on the frontend, a plugin instance is managed independently -- on the backend each request needs to sort out which instance it is and does not have a clear place to initalize/destroy config changes

Perhaps something like:
```
type InstanceRecycler interface {
	Recycle() error
}

type PluginInstance struct {
	CheckHealthHandler  CheckHealthHandler
	CallResourceHandler CallResourceHandler
	QueryDataHandler    QueryDataHandler
	Recycler            InstanceRecycler
}

type PluginHandler interface {
	CreateAppPluginInstance(ctx context.Context, config PluginConfig) (*PluginInstance, error)
	CreateDataSourceInstance(ctx context.Context, config PluginConfig) (*PluginInstance, error)
}
```

The SDK would then manage calls so that:
* Create{DataSource|AppPlugin}Instance was called when appropriate
* the correct instance is called for each health|query|resource|... request
* when the configs change (based on the time parameter) the Recycle function is called

Thoughts?   This is related to #99 -- and hopefully explains why I think this solves @bergquist's issue with the healthcheck clarity

Pull request description:
Move instance management support to datasource package
- Removes the experimental package. 
- Adds new app and datasource package for serving app and datasource plugins.
- New instancemgmt package for managing caching and disposal of instances

Closes #105 

Patch:
diff --git a/backend/data.go b/backend/data.go
index 6fa6293..c00ca94 100644
--- a/backend/data.go
+++ b/backend/data.go
@@ -21,6 +21,17 @@ type QueryDataHandler interface {
 	QueryData(ctx context.Context, req *QueryDataRequest) (*QueryDataResponse, error)
 }
 
+// QueryDataHandlerFunc is an adapter to allow the use of
+// ordinary functions as backend.QueryDataHandler. If f is a function
+// with the appropriate signature, QueryDataHandlerFunc(f) is a
+// Handler that calls f.
+type QueryDataHandlerFunc func(ctx context.Context, req *QueryDataRequest) (*QueryDataResponse, error)
+
+// QueryData calls fn(ctx, req).
+func (fn QueryDataHandlerFunc) QueryData(ctx context.Context, req *QueryDataRequest) (*QueryDataResponse, error) {
+	return fn(ctx, req)
+}
+
 // QueryDataRequest contains a single request which contains multiple queries.
 // It is the input type for a QueryData call.
 type QueryDataRequest struct {
diff --git a/backend/datasource/doc.go b/backend/datasource/doc.go
index 8a61b05..e9f9bc1 100644
--- a/backend/datasource/doc.go
+++ b/backend/datasource/doc.go
@@ -1,2 +1,2 @@
-// Package datasource provides utilities for data source plugins.
+// Package datasource provides utilities for creating and serving a data source plugin over gRPC.
 package datasource
diff --git a/backend/datasource/instance_provider.go b/backend/datasource/instance_provider.go
new file mode 100644
index 0000000..5e2e3ec
--- /dev/null
+++ b/backend/datasource/instance_provider.go
@@ -0,0 +1,59 @@
+package datasource
+
+import (
+	"fmt"
+
+	"github.com/grafana/grafana-plugin-sdk-go/backend"
+	"github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt"
+)
+
+// InstanceFactoryFunc factory method for creating data source instances.
+type InstanceFactoryFunc func(settings backend.DataSourceInstanceSettings) (instancemgmt.Instance, error)
+
+// NewInstanceManager creates a new data source instance manager,
+//
+// This is a helper method for calling NewInstanceProvider and creating a new instancemgmt.InstanceProvider,
+// and providing that to instancemgmt.New.
+func NewInstanceManager(fn InstanceFactoryFunc) instancemgmt.InstanceManager {
+	ip := NewInstanceProvider(fn)
+	return instancemgmt.New(ip)
+}
+
+// NewInstanceProvider create a new data source instance provuder,
+//
+// The instance provider is responsible for providing cache keys for data source instances,
+// creating new instances when needed and invalidating cached instances when they have been
+// updated in Grafana.
+// Cache key is based on the numerical data source identifier.
+// If fn is nil, NewInstanceProvider panics.
+func NewInstanceProvider(fn InstanceFactoryFunc) instancemgmt.InstanceProvider {
+	if fn == nil {
+		panic("fn cannot be nil")
+	}
+
+	return &instanceProvider{
+		factory: fn,
+	}
+}
+
+type instanceProvider struct {
+	factory InstanceFactoryFunc
+}
+
+func (ip *instanceProvider) GetKey(pluginContext backend.PluginContext) (interface{}, error) {
+	if pluginContext.DataSourceInstanceSettings == nil {
+		return nil, fmt.Errorf("data source instance settings cannot be nil")
+	}
+
+	return pluginContext.DataSourceInstanceSettings.ID, nil
+}
+
+func (ip *instanceProvider) NeedsUpdate(pluginContext backend.PluginContext, cachedInstance instancemgmt.CachedInstance) bool {
+	curSettings := pluginContext.DataSourceInstanceSettings
+	cachedSettings := cachedInstance.PluginContext.DataSourceInstanceSettings
+	return !curSettings.Updated.Equal(cachedSettings.Updated)
+}
+
+func (ip *instanceProvider) NewInstance(pluginContext backend.PluginContext) (instancemgmt.Instance, error) {
+	return ip.factory(*pluginContext.DataSourceInstanceSettings)
+}
diff --git a/backend/datasource/query_type_mux.go b/backend/datasource/query_type_mux.go
index 107e624..62596d3 100644
--- a/backend/datasource/query_type_mux.go
+++ b/backend/datasource/query_type_mux.go
@@ -4,22 +4,10 @@ import (
 	"context"
 	"fmt"
 
-	"github.com/grafana/grafana-plugin-sdk-go/backend/log"
-
 	"github.com/grafana/grafana-plugin-sdk-go/backend"
+	"github.com/grafana/grafana-plugin-sdk-go/backend/log"
 )
 
-// QueryTypeHandlerFunc is an adapter to allow the use of
-// ordinary functions as backend.QueryDataHandler. If f is a function
-// with the appropriate signature, QueryTypeHandlerFunc(f) is a
-// Handler that calls f.
-type QueryTypeHandlerFunc func(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error)
-
-// QueryData calls f(ctx, req).
-func (fn QueryTypeHandlerFunc) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
-	return fn(ctx, req)
-}
-
 // QueryTypeMux is a query type multiplexer.
 type QueryTypeMux struct {
 	m               map[string]backend.QueryDataHandler
@@ -44,7 +32,7 @@ func (mux *QueryTypeMux) Handle(queryType string, handler backend.QueryDataHandl
 
 	if mux.m == nil {
 		mux.m = map[string]backend.QueryDataHandler{}
-		mux.fallbackHandler = QueryTypeHandlerFunc(fallbackHandler)
+		mux.fallbackHandler = backend.QueryDataHandlerFunc(fallbackHandler)
 	}
 
 	if _, exist := mux.m[queryType]; exist {
@@ -68,7 +56,7 @@ func (mux *QueryTypeMux) Handle(queryType string, handler backend.QueryDataHandl
 // If handler is nil, Handle panics.
 // If a handler already exists for queryType, Handle panics.
 func (mux *QueryTypeMux) HandleFunc(queryType string, handler func(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error)) {
-	mux.Handle(queryType, QueryTypeHandlerFunc(handler))
+	mux.Handle(queryType, backend.QueryDataHandlerFunc(handler))
 }
 
 // QueryData dispatches the request to the handler(s) whose
diff --git a/backend/datasource/serve.go b/backend/datasource/serve.go
new file mode 100644
index 0000000..5b57045
--- /dev/null
+++ b/backend/datasource/serve.go
@@ -0,0 +1,33 @@
+package datasource
+
+import (
+	"github.com/grafana/grafana-plugin-sdk-go/backend"
+)
+
+// ServeOpts options for serving a data source plugin.
+type ServeOpts struct {
+	// CheckHealthHandler handler for health checks.
+	// Optional to implement.
+	backend.CheckHealthHandler
+
+	// CallResourceHandler handler for resource calls.
+	// Optional to implement.
+	backend.CallResourceHandler
+
+	// QueryDataHandler handler for data queries.
+	// Required to implement.
+	backend.QueryDataHandler
+
+	// GRPCSettings settings for gPRC.
+	GRPCSettings backend.GRPCSettings
+}
+
+// Serve starts serving the data source over gPRC.
+func Serve(opts ServeOpts) error {
+	return backend.Serve(backend.ServeOpts{
+		CheckHealthHandler:  opts.CheckHealthHandler,
+		CallResourceHandler: opts.CallResourceHandler,
+		QueryDataHandler:    opts.QueryDataHandler,
+		GRPCSettings:        opts.GRPCSettings,
+	})
+}
diff --git a/backend/diagnostics.go b/backend/diagnostics.go
index cefa7c9..4592a8a 100644
--- a/backend/diagnostics.go
+++ b/backend/diagnostics.go
@@ -10,6 +10,17 @@ type CheckHealthHandler interface {
 	CheckHealth(ctx context.Context, req *CheckHealthRequest) (*CheckHealthResult, error)
 }
 
+// CheckHealthHandlerFunc is an adapter to allow the use of
+// ordinary functions as backend.CheckHealthHandler. If f is a function
+// with the appropriate signature, CheckHealthHandlerFunc(f) is a
+// Handler that calls f.
+type CheckHealthHandlerFunc func(ctx context.Context, req *CheckHealthRequest) (*CheckHealthResult, error)
+
+// CheckHealth calls fn(ctx, req).
+func (fn CheckHealthHandlerFunc) CheckHealth(ctx context.Context, req *CheckHealthRequest) (*CheckHealthResult, error) {
+	return fn(ctx, req)
+}
+
 // HealthStatus is the status of the plugin.
 type HealthStatus int
 
diff --git a/backend/instancemgmt/doc.go b/backend/instancemgmt/doc.go
new file mode 100644
index 0000000..23cde79
--- /dev/null
+++ b/backend/instancemgmt/doc.go
@@ -0,0 +1,2 @@
+// Package instancemgmt provides utilities for managing plugin instances.
+package instancemgmt
diff --git a/backend/instancemgmt/instance_manager.go b/backend/instancemgmt/instance_manager.go
new file mode 100644
index 0000000..e0dfe43
--- /dev/null
+++ b/backend/instancemgmt/instance_manager.go
@@ -0,0 +1,132 @@
+package instancemgmt
+
+import (
+	"reflect"
+	"sync"
+
+	"github.com/grafana/grafana-plugin-sdk-go/backend"
+)
+
+// Instance is a marker interface for an instance.
+type Instance interface{}
+
+// InstanceDisposer is implemented by an Instance that has a Dispose method,
+// which defines that the instance is disposable.
+//
+// InstanceManager will call the Dispose method before an Instance is replaced
+// with a new Instance. This allows an Instance to clean up resources in use,
+// if any.
+type InstanceDisposer interface {
+	Dispose()
+}
+
+// InstanceCallbackFunc defines the callback function of the InstanceManager.Do method.
+// The argument provided will of type Instance.
+type InstanceCallbackFunc interface{}
+
+// InstanceManager manages the lifecycle of instances.
+type InstanceManager interface {
+	// Get returns an Instance.
+	//
+	// If Instance is cached and not updated it's returned. If Instance is not cached or
+	// updated, a new Instance is created and cached before returned.
+	Get(pluginContext backend.PluginContext) (Instance, error)
+
+	// Do provides an Instance as argument to fn.
+	//
+	// If Instance is cached and not updated provides as argument to fn. If Instance is not cached or
+	// updated, a new Instance is created and cached before provided as argument to fn.
+	Do(pluginContext backend.PluginContext, fn InstanceCallbackFunc) error
+}
+
+// CachedInstance a cached Instance.
+type CachedInstance struct {
+	PluginContext backend.PluginContext
+	instance      Instance
+}
+
+// InstanceProvider defines an instance provider, providing instances.
+type InstanceProvider interface {
+	// GetKey returns a cache key to be used for caching an Instance.
+	GetKey(pluginContext backend.PluginContext) (interface{}, error)
+
+	// NeedsUpdate returns whether a cached Instance have been updated.
+	NeedsUpdate(pluginContext backend.PluginContext, cachedInstance CachedInstance) bool
+
+	// NewInstance creates a new Instance.
+	NewInstance(pluginContext backend.PluginContext) (Instance, error)
+}
+
+// New create a new instance manager.
+func New(provider InstanceProvider) InstanceManager {
+	if provider == nil {
+		panic("provider cannot be nil")
+	}
+
+	return &instanceManager{
+		provider: provider,
+		cache:    map[interface{}]CachedInstance{},
+	}
+}
+
+type instanceManager struct {
+	rwMutex  sync.RWMutex
+	provider InstanceProvider
+	cache    map[interface{}]CachedInstance
+}
+
+func (im *instanceManager) Get(pluginContext backend.PluginContext) (Instance, error) {
+	cacheKey, err := im.provider.GetKey(pluginContext)
+	if err != nil {
+		return nil, err
+	}
+	im.rwMutex.RLock()
+	ci, ok := im.cache[cacheKey]
+	im.rwMutex.RUnlock()
+
+	if ok {
+		needsUpdate := im.provider.NeedsUpdate(pluginContext, ci)
+
+		if !needsUpdate {
+			return ci.instance, nil
+		}
+
+		if disposer, valid := ci.instance.(InstanceDisposer); valid {
+			disposer.Dispose()
+		}
+	}
+
+	im.rwMutex.Lock()
+	defer im.rwMutex.Unlock()
+
+	instance, err := im.provider.NewInstance(pluginContext)
+	if err != nil {
+		return nil, err
+	}
+	im.cache[cacheKey] = CachedInstance{
+		PluginContext: pluginContext,
+		instance:      instance,
+	}
+
+	return instance, nil
+}
+
+func (im *instanceManager) Do(pluginContext backend.PluginContext, fn InstanceCallbackFunc) error {
+	if fn == nil {
+		panic("fn cannot be nil")
+	}
+
+	instance, err := im.Get(pluginContext)
+	if err != nil {
+		return err
+	}
+
+	callInstanceHandlerFunc(fn, instance)
+	return nil
+}
+
+func callInstanceHandlerFunc(fn InstanceCallbackFunc, instance interface{}) {
+	var params = []reflect.Value{}
+	params = append(params, reflect.ValueOf(instance))
+	reflect.ValueOf(fn).Call(params)
+}
diff --git a/backend/resource.go b/backend/resource.go
index 0fa3778..f67adf4 100644
--- a/backend/resource.go
+++ b/backend/resource.go
@@ -30,3 +30,14 @@ type CallResourceResponseSender interface {
 type CallResourceHandler interface {
 	CallResource(ctx context.Context, req *CallResourceRequest, sender CallResourceResponseSender) error
 }
+
+// CallResourceHandlerFunc is an adapter to allow the use of
+// ordinary functions as backend.CallResourceHandler. If f is a function
+// with the appropriate signature, QueryDataHandlerFunc(f) is a
+// Handler that calls f.
+type CallResourceHandlerFunc func(ctx context.Context, req *CallResourceRequest, sender CallResourceResponseSender) error
+
+// CallResource calls fn(ctx, req, sender).
+func (fn CallResourceHandlerFunc) CallResource(ctx context.Context, req *CallResourceRequest, sender CallResourceResponseSender) error {
+	return fn(ctx, req, sender)
+}
diff --git a/backend/resource/doc.go b/backend/resource/doc.go
new file mode 100644
index 0000000..b294661
--- /dev/null
+++ b/backend/resource/doc.go
@@ -0,0 +1,2 @@
+// Package resource provides utils for handling resource calls.
+package resource
diff --git a/experimental/resource.go b/backend/resource/response_util.go
similarity index 74%
rename from experimental/resource.go
rename to backend/resource/response_util.go
index 8190d4c..dc1ab79 100644
--- a/experimental/resource.go
+++ b/backend/resource/response_util.go
@@ -1,16 +1,17 @@
-package experimental
+package resource
 
 import (
 	"encoding/json"
+	"net/http"
 
 	"github.com/grafana/grafana-plugin-sdk-go/backend"
 )
 
-// SendPlainText sends a plain text snippet.
+// SendPlainText sends a plain text response.
 func SendPlainText(sender backend.CallResourceResponseSender, text string) error {
-	return SendResourceResponse(
+	return sendResourceResponse(
 		sender,
-		200,
+		http.StatusOK,
 		map[string][]string{
 			"content-type": {"text/plain"},
 		},
@@ -18,15 +19,15 @@ func SendPlainText(sender backend.CallResourceResponseSender, text string) error
 	)
 }
 
-// SendJSON sends a JSON object.
+// SendJSON sends a JSON response.
 func SendJSON(sender backend.CallResourceResponseSender, obj interface{}) error {
 	body, err := json.Marshal(obj)
 	if err != nil {
 		return err
 	}
-	return SendResourceResponse(
+	return sendResourceResponse(
 		sender,
-		200,
+		http.StatusOK,
 		map[string][]string{
 			"content-type": {"application/json"},
 		},
@@ -34,8 +35,7 @@ func SendJSON(sender backend.CallResourceResponseSender, obj interface{}) error
 	)
 }
 
-// SendResourceResponse sends a JSON object.
-func SendResourceResponse(
+func sendResourceResponse(
 	sender backend.CallResourceResponseSender,
 	status int,
 	headers map[string][]string,
diff --git a/backend/serve.go b/backend/serve.go
index 4c6510d..ade9425 100644
--- a/backend/serve.go
+++ b/backend/serve.go
@@ -8,6 +8,17 @@ import (
 	"google.golang.org/grpc"
 )
 
+// GRPCSettings settings for gRPC.
+type GRPCSettings struct {
+	// MaxReceiveMsgSize the max gRPC message size in bytes the plugin can receive.
+	// If this is <= 0, gRPC uses the default 4MB.
+	MaxReceiveMsgSize int
+
+	// MaxSendMsgSize the max gRPC message size in bytes the plugin can send.
+	// If this is <= 0, gRPC uses the default `math.MaxInt32`.
+	MaxSendMsgSize int
+}
+
 //ServeOpts options for serving plugins.
 type ServeOpts struct {
 	// CheckHealthHandler handler for health checks.
@@ -26,13 +37,8 @@ type ServeOpts struct {
 	// Optional to implement.
 	TransformDataHandler TransformDataHandler
 
-	// MaxGRPCReceiveMsgSize the max gRPC message size in bytes the plugin can receive.
-	// If this is <= 0, gRPC uses the default 4MB.
-	MaxGRPCReceiveMsgSize int
-
-	// MaxGRPCSendMsgSize the max gRPC message size in bytes the plugin can send.
-	// If this is <= 0, gRPC uses the default `math.MaxInt32`.
-	MaxGRPCSendMsgSize int
+	// GRPCSettings settings for gPRC.
+	GRPCSettings GRPCSettings
 }
 
 // Serve starts serving the plugin over gRPC.
@@ -63,12 +69,12 @@ func Serve(opts ServeOpts) error {
 		)),
 	}
 
-	if opts.MaxGRPCReceiveMsgSize > 0 {
-		grpcMiddlewares = append([]grpc.ServerOption{grpc.MaxRecvMsgSize(opts.MaxGRPCReceiveMsgSize)}, grpcMiddlewares...)
+	if opts.GRPCSettings.MaxReceiveMsgSize > 0 {
+		grpcMiddlewares = append([]grpc.ServerOption{grpc.MaxRecvMsgSize(opts.GRPCSettings.MaxReceiveMsgSize)}, grpcMiddlewares...)
 	}
 
-	if opts.MaxGRPCSendMsgSize > 0 {
-		grpcMiddlewares = append([]grpc.ServerOption{grpc.MaxSendMsgSize(opts.MaxGRPCSendMsgSize)}, grpcMiddlewares...)
+	if opts.GRPCSettings.MaxSendMsgSize > 0 {
+		grpcMiddlewares = append([]grpc.ServerOption{grpc.MaxSendMsgSize(opts.GRPCSettings.MaxSendMsgSize)}, grpcMiddlewares...)
 	}
 
 	pluginOpts.GRPCServer = func(opts []grpc.ServerOption) *grpc.Server {
diff --git a/experimental/doc.go b/experimental/doc.go
deleted file mode 100644
index d3c7822..0000000
--- a/experimental/doc.go
+++ /dev/null
@@ -1,4 +0,0 @@
-// Package experimental is a place where we include interfaces and structures that
-// may be useful, but require additional testing and feedback before we think they
-// should be added to the main SDK.  The features in this package may change without notice.
-package experimental
diff --git a/experimental/helper.go b/experimental/helper.go
deleted file mode 100644
index cdf4541..0000000
--- a/experimental/helper.go
+++ /dev/null
@@ -1,132 +0,0 @@
-package experimental
-
-import (
-	"context"
-	"fmt"
-	"sync"
-	"time"
-
-	"github.com/grafana/grafana-plugin-sdk-go/backend"
-)
-
-// InstanceManager is a singleton that holds all datasource instances
-type InstanceManager struct {
-	sync.RWMutex
-
-	instances map[string]instanceInfo
-	host      PluginHost
-}
-
-// NewInstanceManager creates a new instance manager.
-func NewInstanceManager(host PluginHost) *InstanceManager {
-	return &InstanceManager{
-		host:      host,
-		instances: make(map[string]instanceInfo),
-	}
-}
-
-// RunGRPCServer starts the GRPC server
-func (p *InstanceManager) RunGRPCServer() error {
-	return backend.Serve(backend.ServeOpts{
-		CallResourceHandler: p,
-		QueryDataHandler:    p,
-		CheckHealthHandler:  p,
-	})
-}
-
-type instanceInfo struct {
-	// the exe updated time + the datasource updated time
-	// used as a flag to check if anything changed
-	updated int64
-
-	// The raw GRPC values that create the instance
-	ctx backend.PluginContext
-
-	// The specific instance
-	instance DataSourceInstance
-
-	// The last time it was used (so we can expire old things)
-	last time.Time
-}
-
-func (p *InstanceManager) getDataSourceInstance(ctx backend.PluginContext) (DataSourceInstance, error) {
-	if ctx.DataSourceInstanceSettings == nil {
-		return nil, fmt.Errorf("no datasource instance settings provided")
-	}
-	settings := ctx.DataSourceInstanceSettings
-	updated := settings.Updated.UnixNano()
-	key := fmt.Sprintf("%d/%d", ctx.OrgID, settings.ID)
-
-	p.RLock()
-	defer p.RUnlock()
-
-	info, ok := p.instances[key]
-
-	// Check if we need to create a new instance
-	if !ok || updated != info.updated {
-		if ok {
-			info.instance.Destroy()
-		}
-
-		// Create a new one
-		instance, err := p.host.NewDataSourceInstance(ctx)
-		if err != nil {
-			return nil, err
-		}
-
-		info = instanceInfo{
-			updated:  updated,
-			ctx:      ctx,
-			instance: instance,
-		}
-
-		// Set the instance for the key (will replace the old value if exists)
-		p.instances[key] = info
-	}
-	info.last = time.Now()
-	return info.instance, nil
-}
-
-// CheckHealth checks if the plugin is running properly
-func (p *InstanceManager) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) {
-	// 1. Check the datasource config
-	if req.PluginContext.DataSourceInstanceSettings != nil {
-		ds, err := p.getDataSourceInstance(req.PluginContext)
-		if err != nil {
-			// Error reading datasource config
-			return &backend.CheckHealthResult{
-				Status:  backend.HealthStatusError,
-				Message: err.Error(),
-			}, nil
-		}
-		return ds.CheckHealth(), nil
-	}
-
-	// Otherwise the host application
-	return p.host.CheckHostHealth(req.PluginContext), nil
-}
-
-// QueryData queries for data.
-func (p *InstanceManager) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
-	if req.PluginContext.DataSourceInstanceSettings != nil {
-		ds, err := p.getDataSourceInstance(req.PluginContext)
-		if err != nil {
-			return nil, err
-		}
-		return ds.QueryData(req)
-	}
-	return nil, fmt.Errorf("only datasource supports QueryData (for now)")
-}
-
-// CallResource calls a resource.
-func (p *InstanceManager) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error {
-	if req.PluginContext.DataSourceInstanceSettings != nil {
-		ds, err := p.getDataSourceInstance(req.PluginContext)
-		if err != nil {
-			return err
-		}
-		return ds.CallResource(req, sender)
-	}
-
-	return fmt.Errorf("only datasource supports Resources (for now)")
-}
diff --git a/experimental/plugin.go b/experimental/plugin.go
deleted file mode 100644
index 73b67e3..0000000
--- a/experimental/plugin.go
+++ /dev/null
@@ -1,30 +0,0 @@
-package experimental
-
-import (
-	"github.com/grafana/grafana-plugin-sdk-go/backend"
-)
-
-// PluginHost is the singleton container for your plugin.
-type PluginHost interface {
-	// CheckExeHealth corresponds to CheckHostHealth for the plugin executable.
-	CheckHostHealth(ctx backend.PluginContext) *backend.CheckHealthResult
-
-	// NewDataSourceInstance makes a request for a new datasource.
-	NewDataSourceInstance(ctx backend.PluginContext) (DataSourceInstance, error)
-}
-
-// DataSourceInstance implements each of the supported requests.  Alternatively this could be a set
-// of interfaces that work for DataSource|AlertNotifier|etc|etc... with the support interrogated and
-// maintained by the helper on startup.
-type DataSourceInstance interface {
-	CheckHealth() *backend.CheckHealthResult
-
-	// If the request does not need access to the headers or user, use this request
-	QueryData(req *backend.QueryDataRequest) (*backend.QueryDataResponse, error)
-
-	// CallResource calls a resource.
-	CallResource(req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error
-
-	// Destroy lets you clean up any instance variables when the settings change
-	Destroy()
-}
diff --git a/experimental/sample.go b/experimental/sample.go
deleted file mode 100644
index 906927c..0000000
--- a/experimental/sample.go
+++ /dev/null
@@ -1,110 +0,0 @@
-package experimental
-
-import (
-	"fmt"
-	"os"
-
-	"github.com/grafana/grafana-plugin-sdk-go/backend"
-)
-
-//----------------------------------------------------------------------------------
-// PLUGIN HOST
-//----------------------------------------------------------------------------------
-
-// MyHost is a singleton host service.
-type MyHost struct{}
-
-// CheckHostHealth returns a backend.CheckHealthResult.
-func (ds *MyHost) CheckHostHealth(ctx backend.PluginContext) *backend.CheckHealthResult {
-	return &backend.CheckHealthResult{
-		Status:  backend.HealthStatusOk,
-		Message: "Plugin is running",
-	}
-}
-
-// NewDataSourceInstance creates a new datasource instance.
-func (ds *MyHost) NewDataSourceInstance(ctx backend.PluginContext) (DataSourceInstance, error) {
-	settings := myDataSourceSettings{
-		url:  ctx.DataSourceInstanceSettings.URL,
-		port: 1234,
-	}
-
-	return &MyDataSourceInstance{
-		settings: settings,
-		cache:    "TODO",
-		client:   "TODO",
-	}, nil
-}
-
-//----------------------------------------------------------------------------------
-// DATA SOURCE instance
-//----------------------------------------------------------------------------------
-
-type myDataSourceSettings struct {
-	url  string
-	port int32
-}
-
-// MyDataSourceInstance implements backend.DataSourceInstance
-type MyDataSourceInstance struct {
-	settings myDataSourceSettings
-	cache    interface{} // for example
-	client   interface{} // for example
-}
-
-// CheckHealth will check the currently configured settings
-func (ds *MyDataSourceInstance) CheckHealth() *backend.CheckHealthResult {
-	if len(ds.settings.url) < 2 {
-		return &backend.CheckHealthResult{
-			Status:  backend.HealthStatusError,
-			Message: "invalid URL",
-		}
-	}
-	return &backend.CheckHealthResult{
-		Status:  backend.HealthStatusOk,
-		Message: "Datasource is setup properly",
-	}
-}
-
-// QueryData will run a set of queries
-func (ds *MyDataSourceInstance) QueryData(req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
-	return nil, fmt.Errorf("not implemented")
-}
-
-// CallResource HTTP style resource
-func (ds *MyDataSourceInstance) CallResource(req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error {
-
-	if req.Path == "hello" {
-		return SendJSON(sender, map[string]interface{}{"hello": "world"})
-	}
-
-	if req.Path == "text" {
-		return SendPlainText(sender, "hello world")
-	}
-
-	return fmt.Errorf("unknown resource")
-}
-
-// Destroy destroy an instance (if necessary)
-func (ds *MyDataSourceInstance) Destroy() {
-	// If necessary, destroy the object (typically not required)
-}
-
-//----------------------------------------------------------------------------------
-// SAMPLE MAIN
-//----------------------------------------------------------------------------------
-
-// MainXYZ is a sample main function.
-func MainXYZ() {
-	// Setup the plugin environment
-	backend.SetupPluginEnvironment("my-datasource")
-
-	backend.Logger.Debug("Running my backend datasource")
-
-	host := NewInstanceManager(&MyHost{})
-	err := host.RunGRPCServer()
-	if err != nil {
-		backend.Logger.Error(err.Error())
-		os.Exit(1)
-	}
-}
diff --git a/go.mod b/go.mod
index 5ebeaf3..147f79e 100644
--- a/go.mod
+++ b/go.mod
@@ -19,7 +19,7 @@ require (
 	github.com/olekukonko/tablewriter v0.0.4
 	github.com/prometheus/client_golang v1.3.0
 	github.com/prometheus/common v0.7.0
-	github.com/stretchr/testify v1.4.0
+	github.com/stretchr/testify v1.5.1
 	golang.org/x/net v0.0.0-20190923162816-aa69164e4478 // indirect
 	golang.org/x/text v0.3.2 // indirect
 	google.golang.org/grpc v1.27.1
diff --git a/go.sum b/go.sum
index 225dd37..c217905 100644
--- a/go.sum
+++ b/go.sum
@@ -120,6 +120,8 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
+github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
 go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
 go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
 go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=


Test patch:
diff --git a/backend/datasource/instance_provider_test.go b/backend/datasource/instance_provider_test.go
new file mode 100644
index 0000000..19afd99
--- /dev/null
+++ b/backend/datasource/instance_provider_test.go
@@ -0,0 +1,77 @@
+package datasource
+
+import (
+	"testing"
+	"time"
+
+	"github.com/grafana/grafana-plugin-sdk-go/backend"
+	"github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt"
+	"github.com/stretchr/testify/require"
+)
+
+func TestInstanceProvider(t *testing.T) {
+	type testInstance struct {
+		value string
+	}
+	ip := NewInstanceProvider(func(settings backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
+		return testInstance{value: "hello"}, nil
+	})
+
+	t.Run("When data source instance settings not provided should return error", func(t *testing.T) {
+		_, err := ip.GetKey(backend.PluginContext{})
+		require.Error(t, err)
+	})
+
+	t.Run("When data source instance settings provided should return expected key", func(t *testing.T) {
+		key, err := ip.GetKey(backend.PluginContext{
+			DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{
+				ID: 4,
+			},
+		})
+		require.NoError(t, err)
+		require.Equal(t, int64(4), key)
+	})
+
+	t.Run("When current data source instance settings compared to cached instance haven't been updated should return false", func(t *testing.T) {
+		curSettings := backend.PluginContext{
+			DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{
+				Updated: time.Now(),
+			},
+		}
+		cachedInstance := instancemgmt.CachedInstance{
+			PluginContext: backend.PluginContext{
+				DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{
+					Updated: curSettings.DataSourceInstanceSettings.Updated,
+				},
+			},
+		}
+		needsUpdate := ip.NeedsUpdate(curSettings, cachedInstance)
+		require.False(t, needsUpdate)
+	})
+
+	t.Run("When current data source instance settings compared to cached instance have been updated should return true", func(t *testing.T) {
+		curSettings := backend.PluginContext{
+			DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{
+				Updated: time.Now(),
+			},
+		}
+		cachedInstance := instancemgmt.CachedInstance{
+			PluginContext: backend.PluginContext{
+				DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{
+					Updated: curSettings.DataSourceInstanceSettings.Updated.Add(time.Second),
+				},
+			},
+		}
+		needsUpdate := ip.NeedsUpdate(curSettings, cachedInstance)
+		require.True(t, needsUpdate)
+	})
+
+	t.Run("When creating a new instance should return expected instance", func(t *testing.T) {
+		i, err := ip.NewInstance(backend.PluginContext{
+			DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{},
+		})
+		require.NoError(t, err)
+		require.NotNil(t, i)
+		require.Equal(t, "hello", i.(testInstance).value)
+	})
+}
diff --git a/backend/datasource/query_type_mux_example_test.go b/backend/datasource/query_type_mux_example_test.go
new file mode 100644
index 0000000..2f363e0
--- /dev/null
+++ b/backend/datasource/query_type_mux_example_test.go
@@ -0,0 +1,24 @@
+package datasource_test
+
+import (
+	"context"
+
+	"github.com/grafana/grafana-plugin-sdk-go/backend"
+	"github.com/grafana/grafana-plugin-sdk-go/backend/datasource"
+)
+
+func ExampleQueryTypeMux() {
+	mux := datasource.NewQueryTypeMux()
+	mux.HandleFunc("queryTypeA", func(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
+		// handle queryTypeA
+		return nil, nil
+	})
+	mux.HandleFunc("queryTypeB", func(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
+		// handle queryTypeB
+		return nil, nil
+	})
+
+	_ = datasource.ServeOpts{
+		QueryDataHandler: mux,
+	}
+}
diff --git a/backend/datasource/query_type_mux_test.go b/backend/datasource/query_type_mux_test.go
index f379114..736f565 100644
--- a/backend/datasource/query_type_mux_test.go
+++ b/backend/datasource/query_type_mux_test.go
@@ -9,22 +9,6 @@ import (
 	"github.com/stretchr/testify/require"
 )
 
-func ExampleQueryTypeMux() {
-	mux := NewQueryTypeMux()
-	mux.HandleFunc("queryTypeA", func(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
-		// handle queryTypeA
-		return nil, nil
-	})
-	mux.HandleFunc("queryTypeB", func(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
-		// handle queryTypeB
-		return nil, nil
-	})
-
-	_ = backend.ServeOpts{
-		QueryDataHandler: mux,
-	}
-}
-
 func TestQueryTypeMux(t *testing.T) {
 	mux := NewQueryTypeMux()
 	aHandler := &testHandler{}
diff --git a/backend/datasource/serve_example_test.go b/backend/datasource/serve_example_test.go
new file mode 100644
index 0000000..95512c9
--- /dev/null
+++ b/backend/datasource/serve_example_test.go
@@ -0,0 +1,97 @@
+package datasource_test
+
+import (
+	"context"
+	"net/http"
+	"os"
+
+	"github.com/grafana/grafana-plugin-sdk-go/backend"
+	"github.com/grafana/grafana-plugin-sdk-go/backend/datasource"
+	"github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt"
+	"github.com/grafana/grafana-plugin-sdk-go/backend/resource/httpadapter"
+)
+
+type testDataSourceInstanceSettings struct {
+	httpClient *http.Client
+}
+
+func newDataSourceInstance(setting backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
+	return &testDataSourceInstanceSettings{
+		httpClient: &http.Client{},
+	}, nil
+}
+
+func (s *testDataSourceInstanceSettings) Dispose() {
+	// Cleanup
+}
+
+type testDataSource struct {
+	im instancemgmt.InstanceManager
+}
+
+func newDataSource() datasource.ServeOpts {
+	im := datasource.NewInstanceManager(newDataSourceInstance)
+	ds := &testDataSource{
+		im: im,
+	}
+
+	mux := http.NewServeMux()
+	mux.HandleFunc("/test", ds.handleTest)
+
+	return datasource.ServeOpts{
+		CheckHealthHandler:  ds,
+		CallResourceHandler: httpadapter.New(mux),
+		QueryDataHandler:    ds,
+	}
+}
+
+func (ds *testDataSource) getSettings(pluginContext backend.PluginContext) (*testDataSourceInstanceSettings, error) {
+	iface, err := ds.im.Get(pluginContext)
+	if err != nil {
+		return nil, err
+	}
+
+	return iface.(*testDataSourceInstanceSettings), nil
+}
+
+func (ds *testDataSource) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) {
+	settings, err := ds.getSettings(req.PluginContext)
+	if err != nil {
+		return nil, err
+	}
+
+	// Handle request
+	_, _ = settings.httpClient.Get("http://")
+	return nil, nil
+}
+
+func (ds *testDataSource) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
+	var resp *backend.QueryDataResponse
+	err := ds.im.Do(req.PluginContext, func(settings *testDataSourceInstanceSettings) error {
+		// Handle request
+		_, _ = settings.httpClient.Get("http://")
+		return nil
+	})
+
+	return resp, err
+}
+
+func (ds *testDataSource) handleTest(rw http.ResponseWriter, req *http.Request) {
+	pluginContext := httpadapter.PluginConfigFromContext(req.Context())
+	settings, err := ds.getSettings(pluginContext)
+	if err != nil {
+		rw.WriteHeader(500)
+		return
+	}
+
+	// Handle request
+	_, _ = settings.httpClient.Get("http://")
+}
+
+func Example() {
+	err := datasource.Serve(newDataSource())
+	if err != nil {
+		backend.Logger.Error(err.Error())
+		os.Exit(1)
+	}
+}
diff --git a/backend/instancemgmt/instance_manager_test.go b/backend/instancemgmt/instance_manager_test.go
new file mode 100644
index 0000000..60cbaa3
--- /dev/null
+++ b/backend/instancemgmt/instance_manager_test.go
@@ -0,0 +1,90 @@
+package instancemgmt
+
+import (
+	"testing"
+	"time"
+
+	"github.com/grafana/grafana-plugin-sdk-go/backend"
+	"github.com/stretchr/testify/require"
+)
+
+func TestInstanceManager(t *testing.T) {
+	pCtx := backend.PluginContext{
+		OrgID: 1,
+		AppInstanceSettings: &backend.AppInstanceSettings{
+			Updated: time.Now(),
+		},
+	}
+
+	tip := &testInstanceProvider{}
+	im := New(tip)
+
+	t.Run("When getting instance should create a new instance", func(t *testing.T) {
+		instance, err := im.Get(pCtx)
+		require.NoError(t, err)
+		require.NotNil(t, instance)
+		require.Equal(t, pCtx.OrgID, instance.(*testInstance).orgID)
+		require.Equal(t, pCtx.AppInstanceSettings.Updated, instance.(*testInstance).updated)
+
+		t.Run("When getting instance should return same instance", func(t *testing.T) {
+			instance2, err := im.Get(pCtx)
+			require.NoError(t, err)
+			require.Same(t, instance, instance2)
+		})
+
+		t.Run("When updating plugin context and getting instance", func(t *testing.T) {
+			pCtxUpdated := backend.PluginContext{
+				OrgID: 1,
+				AppInstanceSettings: &backend.AppInstanceSettings{
+					Updated: time.Now(),
+				},
+			}
+			newInstance, err := im.Get(pCtxUpdated)
+
+			t.Run("New instance should be created", func(t *testing.T) {
+				require.NoError(t, err)
+				require.NotNil(t, newInstance)
+				require.Equal(t, pCtxUpdated.OrgID, newInstance.(*testInstance).orgID)
+				require.Equal(t, pCtxUpdated.AppInstanceSettings.Updated, newInstance.(*testInstance).updated)
+			})
+
+			t.Run("New instance should not be the same as old instance", func(t *testing.T) {
+				require.NotSame(t, instance, newInstance)
+			})
+
+			t.Run("Old instance should be disposed", func(t *testing.T) {
+				require.True(t, instance.(*testInstance).disposed)
+			})
+		})
+	})
+}
+
+type testInstance struct {
+	orgID    int64
+	updated  time.Time
+	disposed bool
+}
+
+func (ti *testInstance) Dispose() {
+	ti.disposed = true
+}
+
+type testInstanceProvider struct {
+}
+
+func (tip *testInstanceProvider) GetKey(pluginContext backend.PluginContext) (interface{}, error) {
+	return pluginContext.OrgID, nil
+}
+
+func (tip *testInstanceProvider) NeedsUpdate(pluginContext backend.PluginContext, cachedInstance CachedInstance) bool {
+	curUpdated := pluginContext.AppInstanceSettings.Updated
+	cachedUpdated := cachedInstance.PluginContext.AppInstanceSettings.Updated
+	return !curUpdated.Equal(cachedUpdated)
+}
+
+func (tip *testInstanceProvider) NewInstance(pluginContext backend.PluginContext) (Instance, error) {
+	return &testInstance{
+		orgID:   pluginContext.OrgID,
+		updated: pluginContext.AppInstanceSettings.Updated,
+	}, nil
+}