Live: experimental HA with Redis (#34851)

This commit is contained in:
Alexander Emelin
2021-06-24 11:07:09 +03:00
committed by GitHub
parent 437faa72a9
commit 98893c0420
20 changed files with 785 additions and 308 deletions
+11
View File
@@ -901,6 +901,17 @@ plugin_catalog_url = https://grafana.com/grafana/plugins/
# tuning. 0 disables Live, -1 means unlimited connections. # tuning. 0 disables Live, -1 means unlimited connections.
max_connections = 100 max_connections = 100
# engine defines an HA (high availability) engine to use for Grafana Live. By default no engine used - in
# this case Live features work only on a single Grafana server.
# Available options: "redis".
# Setting ha_engine is an EXPERIMENTAL feature.
ha_engine =
# ha_engine_address sets a connection address for Live HA engine. Depending on engine type address format can differ.
# For now we only support Redis connection address in "host:port" format.
# This option is EXPERIMENTAL.
ha_engine_address = "127.0.0.1:6379"
#################################### Grafana Image Renderer Plugin ########################## #################################### Grafana Image Renderer Plugin ##########################
[plugin.grafana-image-renderer] [plugin.grafana-image-renderer]
# Instruct headless browser instance to use a default timezone when not provided by Grafana, e.g. when rendering panel image of alert. # Instruct headless browser instance to use a default timezone when not provided by Grafana, e.g. when rendering panel image of alert.
+10
View File
@@ -887,6 +887,16 @@
# tuning. 0 disables Live, -1 means unlimited connections. # tuning. 0 disables Live, -1 means unlimited connections.
;max_connections = 100 ;max_connections = 100
# engine defines an HA (high availability) engine to use for Grafana Live. By default no engine used - in
# this case Live features work only on a single Grafana server. Available options: "redis".
# Setting ha_engine is an EXPERIMENTAL feature.
;ha_engine =
# ha_engine_address sets a connection address for Live HA engine. Depending on engine type address format can differ.
# For now we only support Redis connection address in "host:port" format.
# This option is EXPERIMENTAL.
;ha_engine_address = "127.0.0.1:6379"
#################################### Grafana Image Renderer Plugin ########################## #################################### Grafana Image Renderer Plugin ##########################
[plugin.grafana-image-renderer] [plugin.grafana-image-renderer]
# Instruct headless browser instance to use a default timezone when not provided by Grafana, e.g. when rendering panel image of alert. # Instruct headless browser instance to use a default timezone when not provided by Grafana, e.g. when rendering panel image of alert.
+1 -1
View File
@@ -23,7 +23,7 @@ require (
github.com/beevik/etree v1.1.0 github.com/beevik/etree v1.1.0
github.com/benbjohnson/clock v1.1.0 github.com/benbjohnson/clock v1.1.0
github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b
github.com/centrifugal/centrifuge v0.17.0 github.com/centrifugal/centrifuge v0.17.1
github.com/cortexproject/cortex v1.8.2-0.20210428155238-d382e1d80eaf github.com/cortexproject/cortex v1.8.2-0.20210428155238-d382e1d80eaf
github.com/crewjam/saml v0.4.6-0.20201227203850-bca570abb2ce github.com/crewjam/saml v0.4.6-0.20201227203850-bca570abb2ce
github.com/davecgh/go-spew v1.1.1 github.com/davecgh/go-spew v1.1.1
+2 -2
View File
@@ -292,8 +292,8 @@ github.com/cenkalti/backoff/v4 v4.1.0 h1:c8LkOFQTzuO0WBM/ae5HdGQuZPfPxp7lqBRwQRm
github.com/cenkalti/backoff/v4 v4.1.0/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= github.com/cenkalti/backoff/v4 v4.1.0/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/centrifugal/centrifuge v0.17.0 h1:ANZMhcR8pFbRUPdv45nrIhhZcsSOdtshT3YM4v1/NHY= github.com/centrifugal/centrifuge v0.17.1 h1:UWORzEE5SEhJSy8omW50AVKbpKaqDBpUFp3kDnMgsXs=
github.com/centrifugal/centrifuge v0.17.0/go.mod h1:AEFs3KPGRpvX1jCe24NDlGWQu7DPa7vdzeY/aUluOm0= github.com/centrifugal/centrifuge v0.17.1/go.mod h1:AEFs3KPGRpvX1jCe24NDlGWQu7DPa7vdzeY/aUluOm0=
github.com/centrifugal/protocol v0.5.0 h1:h71u2Q53yhplftmUk1tjc+Mu6TKJ/eO3YRD3h7Qjvj4= github.com/centrifugal/protocol v0.5.0 h1:h71u2Q53yhplftmUk1tjc+Mu6TKJ/eO3YRD3h7Qjvj4=
github.com/centrifugal/protocol v0.5.0/go.mod h1:ru2N4pwiND/jE+XLtiLYbUo3YmgqgniGNW9f9aRgoVI= github.com/centrifugal/protocol v0.5.0/go.mod h1:ru2N4pwiND/jE+XLtiLYbUo3YmgqgniGNW9f9aRgoVI=
github.com/certifi/gocertifi v0.0.0-20191021191039-0944d244cd40/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA= github.com/certifi/gocertifi v0.0.0-20191021191039-0944d244cd40/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA=
+135 -47
View File
@@ -2,12 +2,10 @@ package live
import ( import (
"context" "context"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"net/http" "net/http"
"net/url" "net/url"
"strconv"
"strings" "strings"
"sync" "sync"
"time" "time"
@@ -26,10 +24,12 @@ import (
"github.com/grafana/grafana/pkg/services/live/database" "github.com/grafana/grafana/pkg/services/live/database"
"github.com/grafana/grafana/pkg/services/live/features" "github.com/grafana/grafana/pkg/services/live/features"
"github.com/grafana/grafana/pkg/services/live/livecontext" "github.com/grafana/grafana/pkg/services/live/livecontext"
"github.com/grafana/grafana/pkg/services/live/liveplugin"
"github.com/grafana/grafana/pkg/services/live/managedstream" "github.com/grafana/grafana/pkg/services/live/managedstream"
"github.com/grafana/grafana/pkg/services/live/orgchannel" "github.com/grafana/grafana/pkg/services/live/orgchannel"
"github.com/grafana/grafana/pkg/services/live/pushws" "github.com/grafana/grafana/pkg/services/live/pushws"
"github.com/grafana/grafana/pkg/services/live/runstream" "github.com/grafana/grafana/pkg/services/live/runstream"
"github.com/grafana/grafana/pkg/services/live/survey"
"github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/services/sqlstore/migrator" "github.com/grafana/grafana/pkg/services/sqlstore/migrator"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
@@ -38,8 +38,8 @@ import (
"github.com/centrifugal/centrifuge" "github.com/centrifugal/centrifuge"
"github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/data"
"github.com/grafana/grafana-plugin-sdk-go/live" "github.com/grafana/grafana-plugin-sdk-go/live"
"gopkg.in/redis.v5"
) )
var ( var (
@@ -84,7 +84,8 @@ type GrafanaLive struct {
DatasourceCache datasources.CacheService `inject:""` DatasourceCache datasources.CacheService `inject:""`
SQLStore *sqlstore.SQLStore `inject:""` SQLStore *sqlstore.SQLStore `inject:""`
node *centrifuge.Node node *centrifuge.Node
surveyCaller *survey.Caller
// Websocket handlers // Websocket handlers
websocketHandler interface{} websocketHandler interface{}
@@ -99,7 +100,7 @@ type GrafanaLive struct {
ManagedStreamRunner *managedstream.Runner ManagedStreamRunner *managedstream.Runner
contextGetter *pluginContextGetter contextGetter *liveplugin.ContextGetter
runStreamManager *runstream.Manager runStreamManager *runstream.Manager
storage *database.Storage storage *database.Storage
} }
@@ -128,17 +129,22 @@ func (g *GrafanaLive) AddMigration(mg *migrator.Migrator) {
func (g *GrafanaLive) Run(ctx context.Context) error { func (g *GrafanaLive) Run(ctx context.Context) error {
if g.runStreamManager != nil { if g.runStreamManager != nil {
// Only run stream manager if GrafanaLive properly initialized. // Only run stream manager if GrafanaLive properly initialized.
return g.runStreamManager.Run(ctx) _ = g.runStreamManager.Run(ctx)
return g.node.Shutdown(context.Background())
} }
return nil return nil
} }
var clientConcurrency = 8 var clientConcurrency = 8
func (g *GrafanaLive) IsHA() bool {
return g.Cfg != nil && g.Cfg.LiveHAEngine != ""
}
// Init initializes Live service. // Init initializes Live service.
// Required to implement the registry.Service interface. // Required to implement the registry.Service interface.
func (g *GrafanaLive) Init() error { func (g *GrafanaLive) Init() error {
logger.Debug("GrafanaLive initialization") logger.Debug("GrafanaLive initialization", "ha", g.IsHA())
// We use default config here as starting point. Default config contains // We use default config here as starting point. Default config contains
// reasonable values for available options. // reasonable values for available options.
@@ -158,10 +164,57 @@ func (g *GrafanaLive) Init() error {
} }
g.node = node g.node = node
g.contextGetter = newPluginContextGetter(g.PluginContextProvider) if g.IsHA() {
channelSender := newPluginChannelSender(node) // Configure HA with Redis. In this case Centrifuge nodes
presenceGetter := newPluginPresenceGetter(node) // will be connected over Redis PUB/SUB. Presence will work
g.runStreamManager = runstream.NewManager(channelSender, presenceGetter, g.contextGetter) // globally since kept inside Redis.
redisAddress := g.Cfg.LiveHAEngineAddress
redisShardConfigs := []centrifuge.RedisShardConfig{
{Address: redisAddress},
}
var redisShards []*centrifuge.RedisShard
for _, redisConf := range redisShardConfigs {
redisShard, err := centrifuge.NewRedisShard(node, redisConf)
if err != nil {
return fmt.Errorf("error connecting to Live Redis: %v", err)
}
redisShards = append(redisShards, redisShard)
}
broker, err := centrifuge.NewRedisBroker(node, centrifuge.RedisBrokerConfig{
Prefix: "gf_live",
// We are using Redis streams here for history. Require Redis >= 5.
UseStreams: true,
// Use reasonably large expiration interval for stream meta key,
// much bigger than maximum HistoryLifetime value in Node config.
// This way stream meta data will expire, in some cases you may want
// to prevent its expiration setting this to zero value.
HistoryMetaTTL: 7 * 24 * time.Hour,
// And configure a couple of shards to use.
Shards: redisShards,
})
if err != nil {
return fmt.Errorf("error creating Live Redis broker: %v", err)
}
node.SetBroker(broker)
presenceManager, err := centrifuge.NewRedisPresenceManager(node, centrifuge.RedisPresenceManagerConfig{
Prefix: "gf_live",
Shards: redisShards,
})
if err != nil {
return fmt.Errorf("error creating Live Redis presence manager: %v", err)
}
node.SetPresenceManager(presenceManager)
}
g.contextGetter = liveplugin.NewContextGetter(g.PluginContextProvider)
channelLocalPublisher := liveplugin.NewChannelLocalPublisher(node)
numLocalSubscribersGetter := liveplugin.NewNumLocalSubscribersGetter(node)
g.runStreamManager = runstream.NewManager(channelLocalPublisher, numLocalSubscribersGetter, g.contextGetter)
// Initialize the main features // Initialize the main features
dash := &features.DashboardHandler{ dash := &features.DashboardHandler{
@@ -173,7 +226,32 @@ func (g *GrafanaLive) Init() error {
g.GrafanaScope.Features["dashboard"] = dash g.GrafanaScope.Features["dashboard"] = dash
g.GrafanaScope.Features["broadcast"] = features.NewBroadcastRunner(g.storage) g.GrafanaScope.Features["broadcast"] = features.NewBroadcastRunner(g.storage)
g.ManagedStreamRunner = managedstream.NewRunner(g.Publish) var managedStreamRunner *managedstream.Runner
if g.IsHA() {
redisClient := redis.NewClient(&redis.Options{
Addr: g.Cfg.LiveHAEngineAddress,
})
cmd := redisClient.Ping()
if _, err := cmd.Result(); err != nil {
return fmt.Errorf("error pinging Redis: %v", err)
}
managedStreamRunner = managedstream.NewRunner(
g.Publish,
managedstream.NewRedisFrameCache(redisClient),
)
} else {
managedStreamRunner = managedstream.NewRunner(
g.Publish,
managedstream.NewMemoryFrameCache(),
)
}
g.ManagedStreamRunner = managedStreamRunner
g.surveyCaller = survey.NewCaller(managedStreamRunner, node)
err = g.surveyCaller.SetupHandlers()
if err != nil {
return err
}
// Set ConnectHandler called when client successfully connected to Node. Your code // Set ConnectHandler called when client successfully connected to Node. Your code
// inside handler must be synchronized since it will be called concurrently from // inside handler must be synchronized since it will be called concurrently from
@@ -222,6 +300,9 @@ func (g *GrafanaLive) Init() error {
reason := "normal" reason := "normal"
if e.Disconnect != nil { if e.Disconnect != nil {
reason = e.Disconnect.Reason reason = e.Disconnect.Reason
if e.Disconnect.Code == 3001 { // Shutdown
return
}
} }
logger.Debug("Client disconnected", "user", client.UserID(), "client", client.ID(), "reason", reason, "elapsed", time.Since(connectedAt)) logger.Debug("Client disconnected", "user", client.UserID(), "client", client.ID(), "reason", reason, "elapsed", time.Since(connectedAt))
}) })
@@ -581,14 +662,7 @@ func (g *GrafanaLive) handleStreamScope(u *models.SignedInUser, namespace string
func (g *GrafanaLive) handleDatasourceScope(user *models.SignedInUser, namespace string) (models.ChannelHandlerFactory, error) { func (g *GrafanaLive) handleDatasourceScope(user *models.SignedInUser, namespace string) (models.ChannelHandlerFactory, error) {
ds, err := g.DatasourceCache.GetDatasourceByUID(namespace, user, false) ds, err := g.DatasourceCache.GetDatasourceByUID(namespace, user, false)
if err != nil { if err != nil {
// the namespace may be an ID return nil, fmt.Errorf("error getting datasource: %w", err)
id, _ := strconv.ParseInt(namespace, 10, 64)
if id > 0 {
ds, err = g.DatasourceCache.GetDatasource(id, user, false)
}
if err != nil {
return nil, fmt.Errorf("error getting datasource: %w", err)
}
} }
streamHandler, err := g.getStreamPlugin(ds.Type) streamHandler, err := g.getStreamPlugin(ds.Type)
if err != nil { if err != nil {
@@ -603,13 +677,13 @@ func (g *GrafanaLive) handleDatasourceScope(user *models.SignedInUser, namespace
), nil ), nil
} }
// Publish sends the data to the channel without checking permissions etc // Publish sends the data to the channel without checking permissions etc.
func (g *GrafanaLive) Publish(orgID int64, channel string, data []byte) error { func (g *GrafanaLive) Publish(orgID int64, channel string, data []byte) error {
_, err := g.node.Publish(orgchannel.PrependOrgID(orgID, channel), data) _, err := g.node.Publish(orgchannel.PrependOrgID(orgID, channel), data)
return err return err
} }
// ClientCount returns the number of clients // ClientCount returns the number of clients.
func (g *GrafanaLive) ClientCount(orgID int64, channel string) (int, error) { func (g *GrafanaLive) ClientCount(orgID int64, channel string) (int, error) {
p, err := g.node.Presence(orgchannel.PrependOrgID(orgID, channel)) p, err := g.node.Presence(orgchannel.PrependOrgID(orgID, channel))
if err != nil { if err != nil {
@@ -652,35 +726,25 @@ func (g *GrafanaLive) HandleHTTPPublish(ctx *models.ReqContext, cmd dtos.LivePub
return response.JSON(http.StatusOK, dtos.LivePublishResponse{}) return response.JSON(http.StatusOK, dtos.LivePublishResponse{})
} }
type streamChannelListResponse struct {
Channels []*managedstream.ManagedChannel `json:"channels"`
}
// HandleListHTTP returns metadata so the UI can build a nice form // HandleListHTTP returns metadata so the UI can build a nice form
func (g *GrafanaLive) HandleListHTTP(c *models.ReqContext) response.Response { func (g *GrafanaLive) HandleListHTTP(c *models.ReqContext) response.Response {
info := util.DynMap{} var channels []*managedstream.ManagedChannel
channels := make([]util.DynMap, 0) var err error
for k, v := range g.ManagedStreamRunner.Streams(c.SignedInUser.OrgId) { if g.IsHA() {
channels = append(channels, v.ListChannels(c.SignedInUser.OrgId, "stream/"+k+"/")...) channels, err = g.surveyCaller.CallManagedStreams(c.SignedInUser.OrgId)
} else {
channels, err = g.ManagedStreamRunner.GetManagedChannels(c.SignedInUser.OrgId)
} }
if err != nil {
// Hardcode sample streams return response.Error(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError), err)
frameJSON, err := data.FrameToJSON(data.NewFrame("testdata", }
data.NewField("Time", nil, make([]time.Time, 0)), info := streamChannelListResponse{
data.NewField("Value", nil, make([]float64, 0)), Channels: channels,
data.NewField("Min", nil, make([]float64, 0)),
data.NewField("Max", nil, make([]float64, 0)),
), data.IncludeSchemaOnly)
if err == nil {
channels = append(channels, util.DynMap{
"channel": "plugin/testdata/random-2s-stream",
"data": json.RawMessage(frameJSON),
}, util.DynMap{
"channel": "plugin/testdata/random-flakey-stream",
"data": json.RawMessage(frameJSON),
}, util.DynMap{
"channel": "plugin/testdata/random-20Hz-stream",
"data": json.RawMessage(frameJSON),
})
} }
info["channels"] = channels
return response.JSONStreaming(200, info) return response.JSONStreaming(200, info)
} }
@@ -696,3 +760,27 @@ func (g *GrafanaLive) HandleInfoHTTP(ctx *models.ReqContext) response.Response {
"message": "Info is not supported for this channel", "message": "Info is not supported for this channel",
}) })
} }
// Write to the standard log15 logger
func handleLog(msg centrifuge.LogEntry) {
arr := make([]interface{}, 0)
for k, v := range msg.Fields {
if v == nil {
v = "<nil>"
} else if v == "" {
v = "<empty>"
}
arr = append(arr, k, v)
}
switch msg.Level {
case centrifuge.LogLevelDebug:
loggerCF.Debug(msg.Message, arr...)
case centrifuge.LogLevelError:
loggerCF.Error(msg.Message, arr...)
case centrifuge.LogLevelInfo:
loggerCF.Info(msg.Message, arr...)
default:
loggerCF.Debug(msg.Message, arr...)
}
}
+56
View File
@@ -0,0 +1,56 @@
package liveplugin
import (
"fmt"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins/plugincontext"
"github.com/centrifugal/centrifuge"
"github.com/grafana/grafana-plugin-sdk-go/backend"
)
type ChannelLocalPublisher struct {
node *centrifuge.Node
}
func NewChannelLocalPublisher(node *centrifuge.Node) *ChannelLocalPublisher {
return &ChannelLocalPublisher{node: node}
}
func (p *ChannelLocalPublisher) PublishLocal(channel string, data []byte) error {
pub := &centrifuge.Publication{
Data: data,
}
err := p.node.Hub().BroadcastPublication(channel, pub, centrifuge.StreamPosition{})
if err != nil {
return fmt.Errorf("error publishing %s: %w", string(data), err)
}
return nil
}
type NumLocalSubscribersGetter struct {
node *centrifuge.Node
}
func NewNumLocalSubscribersGetter(node *centrifuge.Node) *NumLocalSubscribersGetter {
return &NumLocalSubscribersGetter{node: node}
}
func (p *NumLocalSubscribersGetter) GetNumLocalSubscribers(channelID string) (int, error) {
return p.node.Hub().NumSubscribers(channelID), nil
}
type ContextGetter struct {
PluginContextProvider *plugincontext.Provider
}
func NewContextGetter(pluginContextProvider *plugincontext.Provider) *ContextGetter {
return &ContextGetter{
PluginContextProvider: pluginContextProvider,
}
}
func (g *ContextGetter) GetPluginContext(user *models.SignedInUser, pluginID string, datasourceUID string, skipCache bool) (backend.PluginContext, bool, error) {
return g.PluginContextProvider.Get(pluginID, datasourceUID, user, skipCache)
}
-27
View File
@@ -1,27 +0,0 @@
package live
import "github.com/centrifugal/centrifuge"
// Write to the standard log15 logger
func handleLog(msg centrifuge.LogEntry) {
arr := make([]interface{}, 0)
for k, v := range msg.Fields {
if v == nil {
v = "<nil>"
} else if v == "" {
v = "<empty>"
}
arr = append(arr, k, v)
}
switch msg.Level {
case centrifuge.LogLevelDebug:
loggerCF.Debug(msg.Message, arr...)
case centrifuge.LogLevelError:
loggerCF.Error(msg.Message, arr...)
case centrifuge.LogLevelInfo:
loggerCF.Info(msg.Message, arr...)
default:
loggerCF.Debug(msg.Message, arr...)
}
}
+17
View File
@@ -0,0 +1,17 @@
package managedstream
import (
"encoding/json"
"github.com/grafana/grafana-plugin-sdk-go/data"
)
// FrameCache allows updating frame schema. Returns true is schema not changed.
type FrameCache interface {
// GetActiveChannels returns active managed stream channels with JSON schema.
GetActiveChannels(orgID int64) (map[string]json.RawMessage, error)
// GetFrame returns full JSON frame for a path.
GetFrame(orgID int64, channel string) (json.RawMessage, bool, error)
// Update updates frame cache and returns true if schema changed.
Update(orgID int64, channel string, frameJson data.FrameJSONCache) (bool, error)
}
@@ -0,0 +1,54 @@
package managedstream
import (
"encoding/json"
"sync"
"github.com/grafana/grafana-plugin-sdk-go/data"
)
// MemoryFrameCache ...
type MemoryFrameCache struct {
mu sync.RWMutex
frames map[int64]map[string]data.FrameJSONCache
}
// NewMemoryFrameCache ...
func NewMemoryFrameCache() *MemoryFrameCache {
return &MemoryFrameCache{
frames: map[int64]map[string]data.FrameJSONCache{},
}
}
func (c *MemoryFrameCache) GetActiveChannels(orgID int64) (map[string]json.RawMessage, error) {
c.mu.RLock()
defer c.mu.RUnlock()
frames, ok := c.frames[orgID]
if !ok {
return nil, nil
}
info := make(map[string]json.RawMessage, len(frames))
for k, v := range frames {
info[k] = v.Bytes(data.IncludeSchemaOnly)
}
return info, nil
}
func (c *MemoryFrameCache) GetFrame(orgID int64, channel string) (json.RawMessage, bool, error) {
c.mu.RLock()
defer c.mu.RUnlock()
cachedFrame, ok := c.frames[orgID][channel]
return cachedFrame.Bytes(data.IncludeAll), ok, nil
}
func (c *MemoryFrameCache) Update(orgID int64, channel string, jsonFrame data.FrameJSONCache) (bool, error) {
c.mu.Lock()
defer c.mu.Unlock()
if _, ok := c.frames[orgID]; !ok {
c.frames[orgID] = map[string]data.FrameJSONCache{}
}
cachedJsonFrame, exists := c.frames[orgID][channel]
schemaUpdated := !exists || !cachedJsonFrame.SameSchema(&jsonFrame)
c.frames[orgID][channel] = jsonFrame
return schemaUpdated, nil
}
@@ -0,0 +1,69 @@
package managedstream
import (
"encoding/json"
"testing"
"github.com/grafana/grafana-plugin-sdk-go/data"
"github.com/stretchr/testify/require"
)
func testFrameCache(t *testing.T, c FrameCache) {
// Create new frame and update cache.
frame := data.NewFrame("hello")
frameJsonCache, err := data.FrameToJSONCache(frame)
require.NoError(t, err)
updated, err := c.Update(1, "test", frameJsonCache)
require.NoError(t, err)
require.True(t, updated)
// Make sure channel is active.
channels, err := c.GetActiveChannels(1)
require.NoError(t, err)
schema, ok := channels["test"]
require.True(t, ok)
require.NotZero(t, schema)
// Make sure the same frame does not update schema.
updated, err = c.Update(1, "test", frameJsonCache)
require.NoError(t, err)
require.False(t, updated)
// Now construct new frame with updated schema.
newFrame := data.NewFrame("hello", data.NewField("new_field", nil, []int64{}))
frameJsonCache, err = data.FrameToJSONCache(newFrame)
require.NoError(t, err)
// Make sure schema updated.
updated, err = c.Update(1, "test", frameJsonCache)
require.NoError(t, err)
require.True(t, updated)
// Add the same with another orgID and make sure schema updated.
updated, err = c.Update(2, "test", frameJsonCache)
require.NoError(t, err)
require.True(t, updated)
// Make sure that the last frame successfully saved in cache.
frameJSON, ok, err := c.GetFrame(1, "test")
require.NoError(t, err)
require.True(t, ok)
var f data.Frame
err = json.Unmarshal(frameJSON, &f)
require.NoError(t, err)
require.Equal(t, "new_field", f.Fields[0].Name)
// Make sure channel has updated schema.
channels, err = c.GetActiveChannels(1)
require.NoError(t, err)
require.NotEqual(t, string(channels["test"]), string(schema))
}
func TestMemoryFrameCache(t *testing.T) {
c := NewMemoryFrameCache()
require.NotNil(t, c)
testFrameCache(t, c)
}
@@ -0,0 +1,111 @@
package managedstream
import (
"encoding/json"
"errors"
"sync"
"time"
"github.com/grafana/grafana/pkg/services/live/orgchannel"
"github.com/grafana/grafana-plugin-sdk-go/data"
"gopkg.in/redis.v5"
)
// RedisFrameCache ...
type RedisFrameCache struct {
mu sync.RWMutex
redisClient *redis.Client
frames map[int64]map[string]data.FrameJSONCache
}
// NewRedisFrameCache ...
func NewRedisFrameCache(redisClient *redis.Client) *RedisFrameCache {
return &RedisFrameCache{
frames: map[int64]map[string]data.FrameJSONCache{},
redisClient: redisClient,
}
}
func (c *RedisFrameCache) GetActiveChannels(orgID int64) (map[string]json.RawMessage, error) {
c.mu.RLock()
defer c.mu.RUnlock()
frames, ok := c.frames[orgID]
if !ok {
return nil, nil
}
info := make(map[string]json.RawMessage, len(frames))
for k, v := range frames {
info[k] = v.Bytes(data.IncludeSchemaOnly)
}
return info, nil
}
func (c *RedisFrameCache) GetFrame(orgID int64, channel string) (json.RawMessage, bool, error) {
key := getCacheKey(orgchannel.PrependOrgID(orgID, channel))
cmd := c.redisClient.HGetAll(key)
result, err := cmd.Result()
if err != nil {
return nil, false, err
}
if len(result) == 0 {
return nil, false, nil
}
return json.RawMessage(result["frame"]), true, nil
}
const (
frameCacheTTL = 7 * 24 * time.Hour
)
func (c *RedisFrameCache) Update(orgID int64, channel string, jsonFrame data.FrameJSONCache) (bool, error) {
c.mu.Lock()
if _, ok := c.frames[orgID]; !ok {
c.frames[orgID] = map[string]data.FrameJSONCache{}
}
c.frames[orgID][channel] = jsonFrame
c.mu.Unlock()
stringSchema := string(jsonFrame.Bytes(data.IncludeSchemaOnly))
key := getCacheKey(orgchannel.PrependOrgID(orgID, channel))
pipe := c.redisClient.TxPipeline()
defer func() { _ = pipe.Close() }()
pipe.HGetAll(key)
pipe.HMSet(key, map[string]string{
"schema": stringSchema,
"frame": string(jsonFrame.Bytes(data.IncludeAll)),
})
pipe.Expire(key, frameCacheTTL)
replies, err := pipe.Exec()
if err != nil {
return false, err
}
if len(replies) == 0 {
return false, errors.New("no replies in response")
}
reply := replies[0]
if reply.Err() != nil {
return false, err
}
if mapReply, ok := reply.(*redis.StringStringMapCmd); ok {
result, err := mapReply.Result()
if err != nil {
return false, err
}
if len(result) == 0 {
return true, nil
}
return result["schema"] != stringSchema, nil
}
return true, nil
}
func getCacheKey(channelID string) string {
return "gf_live.managed_stream." + channelID
}
@@ -0,0 +1,19 @@
// +build redis
package managedstream
import (
"testing"
"github.com/stretchr/testify/require"
"gopkg.in/redis.v5"
)
func TestRedisCacheStorage(t *testing.T) {
redisClient := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
})
c := NewRedisFrameCache(redisClient)
require.NotNil(t, c)
testFrameCache(t, c)
}
+92 -80
View File
@@ -3,6 +3,7 @@ package managedstream
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"fmt"
"sync" "sync"
"time" "time"
@@ -11,7 +12,6 @@ import (
"github.com/grafana/grafana-plugin-sdk-go/live" "github.com/grafana/grafana-plugin-sdk-go/live"
"github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/util"
) )
var ( var (
@@ -20,19 +20,53 @@ var (
// Runner keeps ManagedStream per streamID. // Runner keeps ManagedStream per streamID.
type Runner struct { type Runner struct {
mu sync.RWMutex mu sync.RWMutex
streams map[int64]map[string]*ManagedStream streams map[int64]map[string]*ManagedStream
publisher models.ChannelPublisher publisher models.ChannelPublisher
frameCache FrameCache
} }
// NewRunner creates new Runner. // NewRunner creates new Runner.
func NewRunner(publisher models.ChannelPublisher) *Runner { func NewRunner(publisher models.ChannelPublisher, frameCache FrameCache) *Runner {
return &Runner{ return &Runner{
publisher: publisher, publisher: publisher,
streams: map[int64]map[string]*ManagedStream{}, streams: map[int64]map[string]*ManagedStream{},
frameCache: frameCache,
} }
} }
func (r *Runner) GetManagedChannels(orgID int64) ([]*ManagedChannel, error) {
channels := make([]*ManagedChannel, 0)
for _, v := range r.Streams(orgID) {
streamChannels, err := v.ListChannels(orgID)
if err != nil {
return nil, err
}
channels = append(channels, streamChannels...)
}
// Hardcode sample streams
frameJSON, err := data.FrameToJSON(data.NewFrame("testdata",
data.NewField("Time", nil, make([]time.Time, 0)),
data.NewField("Value", nil, make([]float64, 0)),
data.NewField("Min", nil, make([]float64, 0)),
data.NewField("Max", nil, make([]float64, 0)),
), data.IncludeSchemaOnly)
if err == nil {
channels = append(channels, &ManagedChannel{
Channel: "plugin/testdata/random-2s-stream",
Data: frameJSON,
}, &ManagedChannel{
Channel: "plugin/testdata/random-flakey-stream",
Data: frameJSON,
}, &ManagedChannel{
Channel: "plugin/testdata/random-20Hz-stream",
Data: frameJSON,
})
}
return channels, nil
}
// Streams returns a map of active managed streams (per streamID). // Streams returns a map of active managed streams (per streamID).
func (r *Runner) Streams(orgID int64) map[string]*ManagedStream { func (r *Runner) Streams(orgID int64) map[string]*ManagedStream {
r.mu.RLock() r.mu.RLock()
@@ -58,7 +92,7 @@ func (r *Runner) GetOrCreateStream(orgID int64, streamID string) (*ManagedStream
} }
s, ok := r.streams[orgID][streamID] s, ok := r.streams[orgID][streamID]
if !ok { if !ok {
s = NewManagedStream(streamID, r.publisher) s = NewManagedStream(streamID, r.publisher, r.frameCache)
r.streams[orgID][streamID] = s r.streams[orgID][streamID] = s
} }
return s, nil return s, nil
@@ -66,112 +100,90 @@ func (r *Runner) GetOrCreateStream(orgID int64, streamID string) (*ManagedStream
// ManagedStream holds the state of a managed stream. // ManagedStream holds the state of a managed stream.
type ManagedStream struct { type ManagedStream struct {
mu sync.RWMutex id string
id string start time.Time
start time.Time publisher models.ChannelPublisher
last map[int64]map[string]data.FrameJSONCache frameCache FrameCache
publisher models.ChannelPublisher
} }
// NewManagedStream creates new ManagedStream. // NewManagedStream creates new ManagedStream.
func NewManagedStream(id string, publisher models.ChannelPublisher) *ManagedStream { func NewManagedStream(id string, publisher models.ChannelPublisher, schemaUpdater FrameCache) *ManagedStream {
return &ManagedStream{ return &ManagedStream{
id: id, id: id,
start: time.Now(), start: time.Now(),
last: map[int64]map[string]data.FrameJSONCache{}, publisher: publisher,
publisher: publisher, frameCache: schemaUpdater,
} }
} }
// ManagedChannel represents a managed stream.
type ManagedChannel struct {
Channel string `json:"channel"`
Data json.RawMessage `json:"data"`
}
// ListChannels returns info for the UI about this stream. // ListChannels returns info for the UI about this stream.
func (s *ManagedStream) ListChannels(orgID int64, prefix string) []util.DynMap { func (s *ManagedStream) ListChannels(orgID int64) ([]*ManagedChannel, error) {
s.mu.RLock() paths, err := s.frameCache.GetActiveChannels(orgID)
defer s.mu.RUnlock() if err != nil {
return []*ManagedChannel{}, fmt.Errorf("error getting active managed stream paths: %v", err)
if _, ok := s.last[orgID]; !ok {
return []util.DynMap{}
} }
info := make([]*ManagedChannel, 0, len(paths))
info := make([]util.DynMap, 0, len(s.last[orgID])) for k, v := range paths {
for k, v := range s.last[orgID] { managedChannel := &ManagedChannel{
ch := util.DynMap{} Channel: k,
ch["channel"] = prefix + k Data: v,
ch["data"] = json.RawMessage(v.Bytes(data.IncludeSchemaOnly)) }
info = append(info, ch) info = append(info, managedChannel)
} }
return info return info, nil
} }
// Push sends frame to the stream and saves it for later retrieval by subscribers. // Push sends frame to the stream and saves it for later retrieval by subscribers.
// unstableSchema flag can be set to disable schema caching for a path. // unstableSchema flag can be set to disable schema caching for a path.
func (s *ManagedStream) Push(orgID int64, path string, frame *data.Frame) error { func (s *ManagedStream) Push(orgID int64, path string, frame *data.Frame) error {
// Keep schema + data for last packet. jsonFrameCache, err := data.FrameToJSONCache(frame)
msg, err := data.FrameToJSONCache(frame)
if err != nil { if err != nil {
logger.Error("Error marshaling frame with data", "error", err)
return err return err
} }
s.mu.Lock()
if _, ok := s.last[orgID]; !ok {
s.last[orgID] = map[string]data.FrameJSONCache{}
}
last, exists := s.last[orgID][path]
s.last[orgID][path] = msg
s.mu.Unlock()
include := data.IncludeAll
if exists && last.SameSchema(&msg) {
// When the schema has not changed, just send the data.
include = data.IncludeDataOnly
}
frameJSON := msg.Bytes(include)
// The channel this will be posted into. // The channel this will be posted into.
channel := live.Channel{Scope: live.ScopeStream, Namespace: s.id, Path: path}.String() channel := live.Channel{Scope: live.ScopeStream, Namespace: s.id, Path: path}.String()
isUpdated, err := s.frameCache.Update(orgID, channel, jsonFrameCache)
if err != nil {
logger.Error("Error updating managed stream schema", "error", err)
return err
}
// When the schema has not changed, just send the data.
include := data.IncludeDataOnly
if isUpdated {
// When the schema has been changed, send all.
include = data.IncludeAll
}
frameJSON := jsonFrameCache.Bytes(include)
logger.Debug("Publish data to channel", "channel", channel, "dataLength", len(frameJSON)) logger.Debug("Publish data to channel", "channel", channel, "dataLength", len(frameJSON))
return s.publisher(orgID, channel, frameJSON) return s.publisher(orgID, channel, frameJSON)
} }
// getLastPacket retrieves last packet channel.
func (s *ManagedStream) getLastPacket(orgId int64, path string) (json.RawMessage, bool) {
s.mu.RLock()
defer s.mu.RUnlock()
_, ok := s.last[orgId]
if !ok {
return nil, false
}
msg, ok := s.last[orgId][path]
if ok {
return msg.Bytes(data.IncludeAll), ok
}
return nil, ok
}
func (s *ManagedStream) GetHandlerForPath(_ string) (models.ChannelHandler, error) { func (s *ManagedStream) GetHandlerForPath(_ string) (models.ChannelHandler, error) {
return s, nil return s, nil
} }
func (s *ManagedStream) OnSubscribe(_ context.Context, u *models.SignedInUser, e models.SubscribeEvent) (models.SubscribeReply, backend.SubscribeStreamStatus, error) { func (s *ManagedStream) OnSubscribe(_ context.Context, u *models.SignedInUser, e models.SubscribeEvent) (models.SubscribeReply, backend.SubscribeStreamStatus, error) {
reply := models.SubscribeReply{} reply := models.SubscribeReply{}
packet, ok := s.getLastPacket(u.OrgId, e.Path) frameJSON, ok, err := s.frameCache.GetFrame(u.OrgId, e.Channel)
if err != nil {
return reply, 0, err
}
if ok { if ok {
reply.Data = packet reply.Data = frameJSON
} }
return reply, backend.SubscribeStreamStatusOK, nil return reply, backend.SubscribeStreamStatusOK, nil
} }
func (s *ManagedStream) OnPublish(_ context.Context, u *models.SignedInUser, evt models.PublishEvent) (models.PublishReply, backend.PublishStreamStatus, error) { func (s *ManagedStream) OnPublish(_ context.Context, _ *models.SignedInUser, _ models.PublishEvent) (models.PublishReply, backend.PublishStreamStatus, error) {
var frame data.Frame return models.PublishReply{}, backend.PublishStreamStatusPermissionDenied, nil
err := json.Unmarshal(evt.Data, &frame)
if err != nil {
// Stream scope only deals with data frames.
return models.PublishReply{}, 0, err
}
err = s.Push(u.OrgId, evt.Path, &frame)
if err != nil {
// Stream scope only deals with data frames.
return models.PublishReply{}, 0, err
}
return models.PublishReply{}, backend.PublishStreamStatusOK, nil
} }
+1 -17
View File
@@ -3,7 +3,6 @@ package managedstream
import ( import (
"testing" "testing"
"github.com/grafana/grafana-plugin-sdk-go/data"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@@ -19,21 +18,6 @@ func (p *testPublisher) publish(orgID int64, _ string, _ []byte) error {
func TestNewManagedStream(t *testing.T) { func TestNewManagedStream(t *testing.T) {
publisher := &testPublisher{orgID: 1, t: t} publisher := &testPublisher{orgID: 1, t: t}
c := NewManagedStream("a", publisher.publish) c := NewManagedStream("a", publisher.publish, NewMemoryFrameCache())
require.NotNil(t, c) require.NotNil(t, c)
} }
func TestManagedStream_GetLastPacket(t *testing.T) {
var orgID int64 = 1
publisher := &testPublisher{orgID: orgID, t: t}
c := NewManagedStream("a", publisher.publish)
_, ok := c.getLastPacket(orgID, "test")
require.False(t, ok)
err := c.Push(orgID, "test", data.NewFrame("hello"))
require.NoError(t, err)
s, ok := c.getLastPacket(orgID, "test")
require.NoError(t, err)
require.True(t, ok)
require.JSONEq(t, `{"schema":{"name":"hello","fields":[]},"data":{"values":[]}}`, string(s))
}
-57
View File
@@ -1,57 +0,0 @@
package live
import (
"fmt"
"github.com/grafana/grafana/pkg/models"
"github.com/centrifugal/centrifuge"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana/pkg/plugins/plugincontext"
)
type pluginChannelSender struct {
node *centrifuge.Node
}
func newPluginChannelSender(node *centrifuge.Node) *pluginChannelSender {
return &pluginChannelSender{node: node}
}
func (p *pluginChannelSender) Send(channel string, data []byte) error {
_, err := p.node.Publish(channel, data)
if err != nil {
return fmt.Errorf("error publishing %s: %w", string(data), err)
}
return nil
}
type pluginPresenceGetter struct {
node *centrifuge.Node
}
func newPluginPresenceGetter(node *centrifuge.Node) *pluginPresenceGetter {
return &pluginPresenceGetter{node: node}
}
func (p *pluginPresenceGetter) GetNumSubscribers(channel string) (int, error) {
res, err := p.node.PresenceStats(channel)
if err != nil {
return 0, err
}
return res.NumClients, nil
}
type pluginContextGetter struct {
PluginContextProvider *plugincontext.Provider
}
func newPluginContextGetter(pluginContextProvider *plugincontext.Provider) *pluginContextGetter {
return &pluginContextGetter{
PluginContextProvider: pluginContextProvider,
}
}
func (g *pluginContextGetter) GetPluginContext(user *models.SignedInUser, pluginID string, datasourceUID string, skipCache bool) (backend.PluginContext, bool, error) {
return g.PluginContextProvider.Get(pluginID, datasourceUID, user, skipCache)
}
+15 -14
View File
@@ -18,18 +18,19 @@ var (
logger = log.New("live.runstream") logger = log.New("live.runstream")
) )
//go:generate mockgen -destination=mock.go -package=runstream github.com/grafana/grafana/pkg/services/live/runstream ChannelSender,PresenceGetter,StreamRunner,PluginContextGetter //go:generate mockgen -destination=mock.go -package=runstream github.com/grafana/grafana/pkg/services/live/runstream ChannelLocalPublisher,NumLocalSubscribersGetter,StreamRunner,PluginContextGetter
type ChannelSender interface { type ChannelLocalPublisher interface {
Send(channel string, data []byte) error PublishLocal(channel string, data []byte) error
} }
type PluginContextGetter interface { type PluginContextGetter interface {
GetPluginContext(user *models.SignedInUser, pluginID string, datasourceUID string, skipCache bool) (backend.PluginContext, bool, error) GetPluginContext(user *models.SignedInUser, pluginID string, datasourceUID string, skipCache bool) (backend.PluginContext, bool, error)
} }
type PresenceGetter interface { type NumLocalSubscribersGetter interface {
GetNumSubscribers(channel string) (int, error) // GetNumSubscribers returns number of channel subscribers throughout all nodes.
GetNumLocalSubscribers(channel string) (int, error)
} }
type StreamRunner interface { type StreamRunner interface {
@@ -37,12 +38,12 @@ type StreamRunner interface {
} }
type packetSender struct { type packetSender struct {
channelSender ChannelSender channelLocalPublisher ChannelLocalPublisher
channel string channel string
} }
func (p *packetSender) Send(packet *backend.StreamPacket) error { func (p *packetSender) Send(packet *backend.StreamPacket) error {
return p.channelSender.Send(p.channel, packet.Data) return p.channelLocalPublisher.PublishLocal(p.channel, packet.Data)
} }
// Manager manages streams from Grafana to plugins (i.e. RunStream method). // Manager manages streams from Grafana to plugins (i.e. RunStream method).
@@ -51,9 +52,9 @@ type Manager struct {
baseCtx context.Context baseCtx context.Context
streams map[string]streamContext streams map[string]streamContext
datasourceStreams map[string]map[string]struct{} datasourceStreams map[string]map[string]struct{}
presenceGetter PresenceGetter presenceGetter NumLocalSubscribersGetter
pluginContextGetter PluginContextGetter pluginContextGetter PluginContextGetter
channelSender ChannelSender channelSender ChannelLocalPublisher
registerCh chan submitRequest registerCh chan submitRequest
closedCh chan struct{} closedCh chan struct{}
checkInterval time.Duration checkInterval time.Duration
@@ -79,7 +80,7 @@ const (
) )
// NewManager creates new Manager. // NewManager creates new Manager.
func NewManager(channelSender ChannelSender, presenceGetter PresenceGetter, pluginContextGetter PluginContextGetter, opts ...ManagerOption) *Manager { func NewManager(channelSender ChannelLocalPublisher, presenceGetter NumLocalSubscribersGetter, pluginContextGetter PluginContextGetter, opts ...ManagerOption) *Manager {
sm := &Manager{ sm := &Manager{
streams: make(map[string]streamContext), streams: make(map[string]streamContext),
datasourceStreams: map[string]map[string]struct{}{}, datasourceStreams: map[string]map[string]struct{}{},
@@ -201,9 +202,9 @@ func (s *Manager) watchStream(ctx context.Context, cancelFn func(), sr streamReq
} }
} }
case <-presenceTicker.C: case <-presenceTicker.C:
numSubscribers, err := s.presenceGetter.GetNumSubscribers(sr.Channel) numSubscribers, err := s.presenceGetter.GetNumLocalSubscribers(sr.Channel)
if err != nil { if err != nil {
logger.Error("Error checking num subscribers", "channel", sr.Channel, "path", sr.Path) logger.Error("Error checking num subscribers", "channel", sr.Channel, "path", sr.Path, "error", err)
continue continue
} }
if numSubscribers > 0 { if numSubscribers > 0 {
@@ -301,7 +302,7 @@ func (s *Manager) runStream(ctx context.Context, cancelFn func(), sr streamReque
PluginContext: pluginCtx, PluginContext: pluginCtx,
Path: sr.Path, Path: sr.Path,
}, },
backend.NewStreamSender(&packetSender{channelSender: s.channelSender, channel: sr.Channel}), backend.NewStreamSender(&packetSender{channelLocalPublisher: s.channelSender, channel: sr.Channel}),
) )
if err != nil { if err != nil {
if errors.Is(ctx.Err(), context.Canceled) { if errors.Is(ctx.Err(), context.Canceled) {
+28 -28
View File
@@ -27,11 +27,11 @@ func TestStreamManager_Run(t *testing.T) {
mockCtrl := gomock.NewController(t) mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish() defer mockCtrl.Finish()
mockChannelSender := NewMockChannelSender(mockCtrl) mockChannelPublisher := NewMockChannelLocalPublisher(mockCtrl)
mockPresenceGetter := NewMockPresenceGetter(mockCtrl) mockNumSubscribersGetter := NewMockNumLocalSubscribersGetter(mockCtrl)
mockContextGetter := NewMockPluginContextGetter(mockCtrl) mockContextGetter := NewMockPluginContextGetter(mockCtrl)
manager := NewManager(mockChannelSender, mockPresenceGetter, mockContextGetter) manager := NewManager(mockChannelPublisher, mockNumSubscribersGetter, mockContextGetter)
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
defer cancel() defer cancel()
@@ -48,11 +48,11 @@ func TestStreamManager_SubmitStream_Send(t *testing.T) {
mockCtrl := gomock.NewController(t) mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish() defer mockCtrl.Finish()
mockPacketSender := NewMockChannelSender(mockCtrl) mockPacketSender := NewMockChannelLocalPublisher(mockCtrl)
mockPresenceGetter := NewMockPresenceGetter(mockCtrl) mockNumSubscribersGetter := NewMockNumLocalSubscribersGetter(mockCtrl)
mockContextGetter := NewMockPluginContextGetter(mockCtrl) mockContextGetter := NewMockPluginContextGetter(mockCtrl)
manager := NewManager(mockPacketSender, mockPresenceGetter, mockContextGetter) manager := NewManager(mockPacketSender, mockNumSubscribersGetter, mockContextGetter)
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
defer cancel() defer cancel()
@@ -79,7 +79,7 @@ func TestStreamManager_SubmitStream_Send(t *testing.T) {
return testPluginContext, true, nil return testPluginContext, true, nil
}).Times(0) }).Times(0)
mockPacketSender.EXPECT().Send("1/test", gomock.Any()).Times(1) mockPacketSender.EXPECT().PublishLocal("1/test", gomock.Any()).Times(1)
mockStreamRunner := NewMockStreamRunner(mockCtrl) mockStreamRunner := NewMockStreamRunner(mockCtrl)
mockStreamRunner.EXPECT().RunStream( mockStreamRunner.EXPECT().RunStream(
@@ -113,11 +113,11 @@ func TestStreamManager_SubmitStream_DifferentOrgID(t *testing.T) {
mockCtrl := gomock.NewController(t) mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish() defer mockCtrl.Finish()
mockPacketSender := NewMockChannelSender(mockCtrl) mockPacketSender := NewMockChannelLocalPublisher(mockCtrl)
mockPresenceGetter := NewMockPresenceGetter(mockCtrl) mockNumSubscribersGetter := NewMockNumLocalSubscribersGetter(mockCtrl)
mockContextGetter := NewMockPluginContextGetter(mockCtrl) mockContextGetter := NewMockPluginContextGetter(mockCtrl)
manager := NewManager(mockPacketSender, mockPresenceGetter, mockContextGetter) manager := NewManager(mockPacketSender, mockNumSubscribersGetter, mockContextGetter)
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
defer cancel() defer cancel()
@@ -130,8 +130,8 @@ func TestStreamManager_SubmitStream_DifferentOrgID(t *testing.T) {
doneCh1 := make(chan struct{}) doneCh1 := make(chan struct{})
doneCh2 := make(chan struct{}) doneCh2 := make(chan struct{})
mockPacketSender.EXPECT().Send("1/test", gomock.Any()).Times(1) mockPacketSender.EXPECT().PublishLocal("1/test", gomock.Any()).Times(1)
mockPacketSender.EXPECT().Send("2/test", gomock.Any()).Times(1) mockPacketSender.EXPECT().PublishLocal("2/test", gomock.Any()).Times(1)
mockContextGetter.EXPECT().GetPluginContext(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(func(user *models.SignedInUser, pluginID string, datasourceUID string, skipCache bool) (backend.PluginContext, bool, error) { mockContextGetter.EXPECT().GetPluginContext(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(func(user *models.SignedInUser, pluginID string, datasourceUID string, skipCache bool) (backend.PluginContext, bool, error) {
return backend.PluginContext{}, true, nil return backend.PluginContext{}, true, nil
@@ -184,14 +184,14 @@ func TestStreamManager_SubmitStream_CloseNoSubscribers(t *testing.T) {
mockCtrl := gomock.NewController(t) mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish() defer mockCtrl.Finish()
mockPacketSender := NewMockChannelSender(mockCtrl) mockPacketSender := NewMockChannelLocalPublisher(mockCtrl)
mockPresenceGetter := NewMockPresenceGetter(mockCtrl) mockNumSubscribersGetter := NewMockNumLocalSubscribersGetter(mockCtrl)
mockContextGetter := NewMockPluginContextGetter(mockCtrl) mockContextGetter := NewMockPluginContextGetter(mockCtrl)
// Create manager with very fast num subscribers checks. // Create manager with very fast num subscribers checks.
manager := NewManager( manager := NewManager(
mockPacketSender, mockPacketSender,
mockPresenceGetter, mockNumSubscribersGetter,
mockContextGetter, mockContextGetter,
WithCheckConfig(10*time.Millisecond, 3), WithCheckConfig(10*time.Millisecond, 3),
) )
@@ -209,7 +209,7 @@ func TestStreamManager_SubmitStream_CloseNoSubscribers(t *testing.T) {
return backend.PluginContext{}, true, nil return backend.PluginContext{}, true, nil
}).Times(0) }).Times(0)
mockPresenceGetter.EXPECT().GetNumSubscribers("1/test").Return(0, nil).Times(3) mockNumSubscribersGetter.EXPECT().GetNumLocalSubscribers("1/test").Return(0, nil).Times(3)
mockStreamRunner := NewMockStreamRunner(mockCtrl) mockStreamRunner := NewMockStreamRunner(mockCtrl)
mockStreamRunner.EXPECT().RunStream(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, req *backend.RunStreamRequest, sender *backend.StreamSender) error { mockStreamRunner.EXPECT().RunStream(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, req *backend.RunStreamRequest, sender *backend.StreamSender) error {
@@ -231,11 +231,11 @@ func TestStreamManager_SubmitStream_ErrorRestartsRunStream(t *testing.T) {
mockCtrl := gomock.NewController(t) mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish() defer mockCtrl.Finish()
mockPacketSender := NewMockChannelSender(mockCtrl) mockPacketSender := NewMockChannelLocalPublisher(mockCtrl)
mockPresenceGetter := NewMockPresenceGetter(mockCtrl) mockNumSubscribersGetter := NewMockNumLocalSubscribersGetter(mockCtrl)
mockContextGetter := NewMockPluginContextGetter(mockCtrl) mockContextGetter := NewMockPluginContextGetter(mockCtrl)
manager := NewManager(mockPacketSender, mockPresenceGetter, mockContextGetter) manager := NewManager(mockPacketSender, mockNumSubscribersGetter, mockContextGetter)
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
defer cancel() defer cancel()
@@ -284,11 +284,11 @@ func TestStreamManager_SubmitStream_NilErrorStopsRunStream(t *testing.T) {
mockCtrl := gomock.NewController(t) mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish() defer mockCtrl.Finish()
mockPacketSender := NewMockChannelSender(mockCtrl) mockPacketSender := NewMockChannelLocalPublisher(mockCtrl)
mockPresenceGetter := NewMockPresenceGetter(mockCtrl) mockNumSubscribersGetter := NewMockNumLocalSubscribersGetter(mockCtrl)
mockContextGetter := NewMockPluginContextGetter(mockCtrl) mockContextGetter := NewMockPluginContextGetter(mockCtrl)
manager := NewManager(mockPacketSender, mockPresenceGetter, mockContextGetter) manager := NewManager(mockPacketSender, mockNumSubscribersGetter, mockContextGetter)
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
defer cancel() defer cancel()
@@ -317,11 +317,11 @@ func TestStreamManager_HandleDatasourceUpdate(t *testing.T) {
mockCtrl := gomock.NewController(t) mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish() defer mockCtrl.Finish()
mockPacketSender := NewMockChannelSender(mockCtrl) mockPacketSender := NewMockChannelLocalPublisher(mockCtrl)
mockPresenceGetter := NewMockPresenceGetter(mockCtrl) mockNumSubscribersGetter := NewMockNumLocalSubscribersGetter(mockCtrl)
mockContextGetter := NewMockPluginContextGetter(mockCtrl) mockContextGetter := NewMockPluginContextGetter(mockCtrl)
manager := NewManager(mockPacketSender, mockPresenceGetter, mockContextGetter) manager := NewManager(mockPacketSender, mockNumSubscribersGetter, mockContextGetter)
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
defer cancel() defer cancel()
@@ -383,11 +383,11 @@ func TestStreamManager_HandleDatasourceDelete(t *testing.T) {
mockCtrl := gomock.NewController(t) mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish() defer mockCtrl.Finish()
mockPacketSender := NewMockChannelSender(mockCtrl) mockPacketSender := NewMockChannelLocalPublisher(mockCtrl)
mockPresenceGetter := NewMockPresenceGetter(mockCtrl) mockNumSubscribersGetter := NewMockNumLocalSubscribersGetter(mockCtrl)
mockContextGetter := NewMockPluginContextGetter(mockCtrl) mockContextGetter := NewMockPluginContextGetter(mockCtrl)
manager := NewManager(mockPacketSender, mockPresenceGetter, mockContextGetter) manager := NewManager(mockPacketSender, mockNumSubscribersGetter, mockContextGetter)
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
defer cancel() defer cancel()
+35 -35
View File
@@ -1,5 +1,5 @@
// Code generated by MockGen. DO NOT EDIT. // Code generated by MockGen. DO NOT EDIT.
// Source: github.com/grafana/grafana/pkg/services/live/runstream (interfaces: ChannelSender,PresenceGetter,StreamRunner,PluginContextGetter) // Source: github.com/grafana/grafana/pkg/services/live/runstream (interfaces: ChannelLocalPublisher,NumLocalSubscribersGetter,StreamRunner,PluginContextGetter)
// Package runstream is a generated GoMock package. // Package runstream is a generated GoMock package.
package runstream package runstream
@@ -13,79 +13,79 @@ import (
models "github.com/grafana/grafana/pkg/models" models "github.com/grafana/grafana/pkg/models"
) )
// MockChannelSender is a mock of ChannelSender interface. // MockChannelLocalPublisher is a mock of ChannelLocalPublisher interface.
type MockChannelSender struct { type MockChannelLocalPublisher struct {
ctrl *gomock.Controller ctrl *gomock.Controller
recorder *MockChannelSenderMockRecorder recorder *MockChannelLocalPublisherMockRecorder
} }
// MockChannelSenderMockRecorder is the mock recorder for MockChannelSender. // MockChannelLocalPublisherMockRecorder is the mock recorder for MockChannelLocalPublisher.
type MockChannelSenderMockRecorder struct { type MockChannelLocalPublisherMockRecorder struct {
mock *MockChannelSender mock *MockChannelLocalPublisher
} }
// NewMockChannelSender creates a new mock instance. // NewMockChannelLocalPublisher creates a new mock instance.
func NewMockChannelSender(ctrl *gomock.Controller) *MockChannelSender { func NewMockChannelLocalPublisher(ctrl *gomock.Controller) *MockChannelLocalPublisher {
mock := &MockChannelSender{ctrl: ctrl} mock := &MockChannelLocalPublisher{ctrl: ctrl}
mock.recorder = &MockChannelSenderMockRecorder{mock} mock.recorder = &MockChannelLocalPublisherMockRecorder{mock}
return mock return mock
} }
// EXPECT returns an object that allows the caller to indicate expected use. // EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockChannelSender) EXPECT() *MockChannelSenderMockRecorder { func (m *MockChannelLocalPublisher) EXPECT() *MockChannelLocalPublisherMockRecorder {
return m.recorder return m.recorder
} }
// Send mocks base method. // PublishLocal mocks base method.
func (m *MockChannelSender) Send(arg0 string, arg1 []byte) error { func (m *MockChannelLocalPublisher) PublishLocal(arg0 string, arg1 []byte) error {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Send", arg0, arg1) ret := m.ctrl.Call(m, "PublishLocal", arg0, arg1)
ret0, _ := ret[0].(error) ret0, _ := ret[0].(error)
return ret0 return ret0
} }
// Send indicates an expected call of Send. // PublishLocal indicates an expected call of PublishLocal.
func (mr *MockChannelSenderMockRecorder) Send(arg0, arg1 interface{}) *gomock.Call { func (mr *MockChannelLocalPublisherMockRecorder) PublishLocal(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Send", reflect.TypeOf((*MockChannelSender)(nil).Send), arg0, arg1) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PublishLocal", reflect.TypeOf((*MockChannelLocalPublisher)(nil).PublishLocal), arg0, arg1)
} }
// MockPresenceGetter is a mock of PresenceGetter interface. // MockNumLocalSubscribersGetter is a mock of NumLocalSubscribersGetter interface.
type MockPresenceGetter struct { type MockNumLocalSubscribersGetter struct {
ctrl *gomock.Controller ctrl *gomock.Controller
recorder *MockPresenceGetterMockRecorder recorder *MockNumLocalSubscribersGetterMockRecorder
} }
// MockPresenceGetterMockRecorder is the mock recorder for MockPresenceGetter. // MockNumLocalSubscribersGetterMockRecorder is the mock recorder for MockNumLocalSubscribersGetter.
type MockPresenceGetterMockRecorder struct { type MockNumLocalSubscribersGetterMockRecorder struct {
mock *MockPresenceGetter mock *MockNumLocalSubscribersGetter
} }
// NewMockPresenceGetter creates a new mock instance. // NewMockNumLocalSubscribersGetter creates a new mock instance.
func NewMockPresenceGetter(ctrl *gomock.Controller) *MockPresenceGetter { func NewMockNumLocalSubscribersGetter(ctrl *gomock.Controller) *MockNumLocalSubscribersGetter {
mock := &MockPresenceGetter{ctrl: ctrl} mock := &MockNumLocalSubscribersGetter{ctrl: ctrl}
mock.recorder = &MockPresenceGetterMockRecorder{mock} mock.recorder = &MockNumLocalSubscribersGetterMockRecorder{mock}
return mock return mock
} }
// EXPECT returns an object that allows the caller to indicate expected use. // EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockPresenceGetter) EXPECT() *MockPresenceGetterMockRecorder { func (m *MockNumLocalSubscribersGetter) EXPECT() *MockNumLocalSubscribersGetterMockRecorder {
return m.recorder return m.recorder
} }
// GetNumSubscribers mocks base method. // GetNumLocalSubscribers mocks base method.
func (m *MockPresenceGetter) GetNumSubscribers(arg0 string) (int, error) { func (m *MockNumLocalSubscribersGetter) GetNumLocalSubscribers(arg0 string) (int, error) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetNumSubscribers", arg0) ret := m.ctrl.Call(m, "GetNumLocalSubscribers", arg0)
ret0, _ := ret[0].(int) ret0, _ := ret[0].(int)
ret1, _ := ret[1].(error) ret1, _ := ret[1].(error)
return ret0, ret1 return ret0, ret1
} }
// GetNumSubscribers indicates an expected call of GetNumSubscribers. // GetNumLocalSubscribers indicates an expected call of GetNumLocalSubscribers.
func (mr *MockPresenceGetterMockRecorder) GetNumSubscribers(arg0 interface{}) *gomock.Call { func (mr *MockNumLocalSubscribersGetterMockRecorder) GetNumLocalSubscribers(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNumSubscribers", reflect.TypeOf((*MockPresenceGetter)(nil).GetNumSubscribers), arg0) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNumLocalSubscribers", reflect.TypeOf((*MockNumLocalSubscribersGetter)(nil).GetNumLocalSubscribers), arg0)
} }
// MockStreamRunner is a mock of StreamRunner interface. // MockStreamRunner is a mock of StreamRunner interface.
+117
View File
@@ -0,0 +1,117 @@
package survey
import (
"context"
"encoding/json"
"errors"
"fmt"
"time"
"github.com/centrifugal/centrifuge"
"github.com/grafana/grafana/pkg/services/live/managedstream"
)
type Caller struct {
managedStreamRunner *managedstream.Runner
node *centrifuge.Node
}
const (
managedStreamsCall = "managed_streams"
)
func NewCaller(managedStreamRunner *managedstream.Runner, node *centrifuge.Node) *Caller {
return &Caller{managedStreamRunner: managedStreamRunner, node: node}
}
func (c *Caller) SetupHandlers() error {
c.node.OnSurvey(c.handleSurvey)
return nil
}
type NodeManagedChannelsRequest struct {
OrgID int64 `json:"orgId"`
}
type NodeManagedChannelsResponse struct {
Channels []*managedstream.ManagedChannel `json:"channels"`
}
func (c *Caller) handleSurvey(e centrifuge.SurveyEvent, cb centrifuge.SurveyCallback) {
var (
resp interface{}
err error
)
switch e.Op {
case managedStreamsCall:
resp, err = c.handleManagedStreams(e.Data)
default:
err = errors.New("method not found")
}
if err != nil {
cb(centrifuge.SurveyReply{Code: 1})
return
}
jsonData, err := json.Marshal(resp)
if err != nil {
cb(centrifuge.SurveyReply{Code: 1})
return
}
cb(centrifuge.SurveyReply{
Code: 0,
Data: jsonData,
})
}
func (c *Caller) handleManagedStreams(data []byte) (interface{}, error) {
var req NodeManagedChannelsRequest
err := json.Unmarshal(data, &req)
if err != nil {
return nil, err
}
channels, err := c.managedStreamRunner.GetManagedChannels(req.OrgID)
if err != nil {
return nil, err
}
return NodeManagedChannelsResponse{
Channels: channels,
}, nil
}
func (c *Caller) CallManagedStreams(orgID int64) ([]*managedstream.ManagedChannel, error) {
req := NodeManagedChannelsRequest{OrgID: orgID}
jsonData, err := json.Marshal(req)
if err != nil {
return nil, err
}
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
resp, err := c.node.Survey(ctx, managedStreamsCall, jsonData)
if err != nil {
return nil, err
}
channels := make([]*managedstream.ManagedChannel, 0)
duplicatesCheck := map[string]struct{}{}
for _, result := range resp {
if result.Code != 0 {
return nil, fmt.Errorf("unexpected survey code: %d", result.Code)
}
var res NodeManagedChannelsResponse
err := json.Unmarshal(result.Data, &res)
if err != nil {
return nil, err
}
for _, ch := range res.Channels {
if _, ok := duplicatesCheck[ch.Channel]; ok {
continue
}
channels = append(channels, ch)
duplicatesCheck[ch.Channel] = struct{}{}
}
}
return channels, nil
}
+12
View File
@@ -386,6 +386,11 @@ type Cfg struct {
// Grafana Live ws endpoint (per Grafana server instance). 0 disables // Grafana Live ws endpoint (per Grafana server instance). 0 disables
// Live, -1 means unlimited connections. // Live, -1 means unlimited connections.
LiveMaxConnections int LiveMaxConnections int
// LiveHAEngine is a type of engine to use to achieve HA with Grafana Live.
// Zero value means in-memory single node setup.
LiveHAEngine string
// LiveHAEngineAddress is a connection address for Live HA engine.
LiveHAEngineAddress string
// Grafana.com URL // Grafana.com URL
GrafanaComURL string GrafanaComURL string
@@ -1447,5 +1452,12 @@ func (cfg *Cfg) readLiveSettings(iniFile *ini.File) error {
if cfg.LiveMaxConnections < -1 { if cfg.LiveMaxConnections < -1 {
return fmt.Errorf("unexpected value %d for [live] max_connections", cfg.LiveMaxConnections) return fmt.Errorf("unexpected value %d for [live] max_connections", cfg.LiveMaxConnections)
} }
cfg.LiveHAEngine = section.Key("ha_engine").MustString("")
switch cfg.LiveHAEngine {
case "", "redis":
default:
return fmt.Errorf("unsupported live HA engine type: %s", cfg.LiveHAEngine)
}
cfg.LiveHAEngineAddress = section.Key("ha_engine_address").MustString("127.0.0.1:6379")
return nil return nil
} }