Rendering service - add optional RenderingSession (#44098)

* rendering service changes:
- make node-renderer request timeout configurable
- introduce optional RenderingSession providing a long-lived session key

* remove console logs

* added comment explaining empty "afterRequest" method

* fix compilation error

* update imports formatting

* Update pkg/services/rendering/interface.go

Co-authored-by: Agnès Toulet <35176601+AgnesToulet@users.noreply.github.com>

* Update pkg/services/rendering/rendering.go

Co-authored-by: Agnès Toulet <35176601+AgnesToulet@users.noreply.github.com>

* review fix: extract renderKey related functions/structs to auth.go

* #44449: private'd `rendering.getRequestTimeout`

Co-authored-by: Agnès Toulet <35176601+AgnesToulet@users.noreply.github.com>
pull/45191/head
Artur Wierzbicki 3 years ago committed by GitHub
parent 3c334cd8ba
commit 5148250366
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 14
      pkg/api/render.go
  2. 12
      pkg/services/alerting/notifier.go
  3. 8
      pkg/services/alerting/notifier_test.go
  4. 120
      pkg/services/rendering/auth.go
  5. 4
      pkg/services/rendering/http_mode.go
  6. 51
      pkg/services/rendering/interface.go
  7. 5
      pkg/services/rendering/plugin_mode.go
  8. 93
      pkg/services/rendering/rendering.go
  9. 2
      pkg/services/rendering/rendering_test.go
  10. 21
      pkg/services/thumbs/crawler.go

@ -53,12 +53,16 @@ func (hs *HTTPServer) RenderToPng(c *models.ReqContext) {
}
result, err := hs.RenderService.Render(c.Req.Context(), rendering.Opts{
TimeoutOpts: rendering.TimeoutOpts{
Timeout: time.Duration(timeout) * time.Second,
},
AuthOpts: rendering.AuthOpts{
OrgID: c.OrgId,
UserID: c.UserId,
OrgRole: c.OrgRole,
},
Width: width,
Height: height,
Timeout: time.Duration(timeout) * time.Second,
OrgID: c.OrgId,
UserID: c.UserId,
OrgRole: c.OrgRole,
Path: web.Params(c.Req)["*"] + queryParams,
Timezone: queryReader.Get("tz", ""),
Encoding: queryReader.Get("encoding", ""),
@ -66,7 +70,7 @@ func (hs *HTTPServer) RenderToPng(c *models.ReqContext) {
DeviceScaleFactor: scale,
Headers: headers,
Theme: rendering.ThemeDark,
})
}, nil)
if err != nil {
if errors.Is(err, rendering.ErrTimeout) {
c.Handle(hs.Cfg, 500, err.Error(), err)

@ -200,11 +200,15 @@ func (n *notificationService) renderAndUploadImage(evalCtx *EvalContext, timeout
}
renderOpts := rendering.Opts{
TimeoutOpts: rendering.TimeoutOpts{
Timeout: timeout,
},
AuthOpts: rendering.AuthOpts{
OrgID: evalCtx.Rule.OrgID,
OrgRole: models.ROLE_ADMIN,
},
Width: 1000,
Height: 500,
Timeout: timeout,
OrgID: evalCtx.Rule.OrgID,
OrgRole: models.ROLE_ADMIN,
ConcurrentLimit: setting.AlertingRenderLimit,
Theme: rendering.ThemeDark,
}
@ -218,7 +222,7 @@ func (n *notificationService) renderAndUploadImage(evalCtx *EvalContext, timeout
n.log.Debug("Rendering alert panel image", "ruleId", evalCtx.Rule.ID, "urlPath", renderOpts.Path)
start := time.Now()
result, err := n.renderService.Render(evalCtx.Ctx, renderOpts)
result, err := n.renderService.Render(evalCtx.Ctx, renderOpts, nil)
if err != nil {
return err
}

@ -353,7 +353,7 @@ func (s *testRenderService) IsAvailable() bool {
return true
}
func (s *testRenderService) Render(ctx context.Context, opts rendering.Opts) (*rendering.RenderResult, error) {
func (s *testRenderService) Render(ctx context.Context, opts rendering.Opts, session rendering.Session) (*rendering.RenderResult, error) {
if s.renderProvider != nil {
return s.renderProvider(ctx, opts)
}
@ -361,7 +361,7 @@ func (s *testRenderService) Render(ctx context.Context, opts rendering.Opts) (*r
return &rendering.RenderResult{FilePath: "image.png"}, nil
}
func (s *testRenderService) RenderCSV(ctx context.Context, opts rendering.CSVOpts) (*rendering.RenderCSVResult, error) {
func (s *testRenderService) RenderCSV(ctx context.Context, opts rendering.CSVOpts, session rendering.Session) (*rendering.RenderCSVResult, error) {
return nil, nil
}
@ -381,6 +381,10 @@ func (s *testRenderService) Version() string {
return ""
}
func (s *testRenderService) CreateRenderingSession(ctx context.Context, authOpts rendering.AuthOpts, sessionOpts rendering.SessionOpts) (rendering.Session, error) {
return nil, nil
}
var _ rendering.Service = &testRenderService{}
type testImageUploader struct {

@ -0,0 +1,120 @@
package rendering
import (
"context"
"fmt"
"time"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/remotecache"
"github.com/grafana/grafana/pkg/util"
)
const renderKeyPrefix = "render-%s"
type RenderUser struct {
OrgID int64
UserID int64
OrgRole string
}
func (rs *RenderingService) GetRenderUser(ctx context.Context, key string) (*RenderUser, bool) {
val, err := rs.RemoteCacheService.Get(ctx, fmt.Sprintf(renderKeyPrefix, key))
if err != nil {
rs.log.Error("Failed to get render key from cache", "error", err)
}
if val != nil {
if user, ok := val.(*RenderUser); ok {
return user, true
}
}
return nil, false
}
func setRenderKey(cache *remotecache.RemoteCache, ctx context.Context, opts AuthOpts, renderKey string, expiry time.Duration) error {
err := cache.Set(ctx, fmt.Sprintf(renderKeyPrefix, renderKey), &RenderUser{
OrgID: opts.OrgID,
UserID: opts.UserID,
OrgRole: string(opts.OrgRole),
}, expiry)
return err
}
func generateAndSetRenderKey(cache *remotecache.RemoteCache, ctx context.Context, opts AuthOpts, expiry time.Duration) (string, error) {
key, err := util.GetRandomString(32)
if err != nil {
return "", err
}
err = setRenderKey(cache, ctx, opts, key, expiry)
if err != nil {
return "", err
}
return key, nil
}
type longLivedRenderKeyProvider struct {
cache *remotecache.RemoteCache
log log.Logger
renderKey string
authOpts AuthOpts
sessionOpts SessionOpts
}
func (rs *RenderingService) CreateRenderingSession(ctx context.Context, opts AuthOpts, sessionOpts SessionOpts) (Session, error) {
renderKey, err := generateAndSetRenderKey(rs.RemoteCacheService, ctx, opts, sessionOpts.Expiry)
if err != nil {
return nil, err
}
return &longLivedRenderKeyProvider{
log: rs.log,
renderKey: renderKey,
cache: rs.RemoteCacheService,
authOpts: opts,
sessionOpts: sessionOpts,
}, nil
}
func deleteRenderKey(cache *remotecache.RemoteCache, log log.Logger, ctx context.Context, renderKey string) {
err := cache.Delete(ctx, fmt.Sprintf(renderKeyPrefix, renderKey))
if err != nil {
log.Error("Failed to delete render key", "error", err)
}
}
type perRequestRenderKeyProvider struct {
cache *remotecache.RemoteCache
log log.Logger
keyExpiry time.Duration
}
func (r *perRequestRenderKeyProvider) get(ctx context.Context, opts AuthOpts) (string, error) {
return generateAndSetRenderKey(r.cache, ctx, opts, r.keyExpiry)
}
func (r *perRequestRenderKeyProvider) afterRequest(ctx context.Context, opts AuthOpts, renderKey string) {
deleteRenderKey(r.cache, r.log, ctx, renderKey)
}
func (r *longLivedRenderKeyProvider) get(ctx context.Context, opts AuthOpts) (string, error) {
if r.sessionOpts.RefreshExpiryOnEachRequest {
err := setRenderKey(r.cache, ctx, opts, r.renderKey, r.sessionOpts.Expiry)
if err != nil {
r.log.Error("Failed to refresh render key", "error", err, "renderKey", r.renderKey)
}
}
return r.renderKey, nil
}
func (r *longLivedRenderKeyProvider) afterRequest(ctx context.Context, opts AuthOpts, renderKey string) {
// do nothing - renderKey from longLivedRenderKeyProvider is deleted only after session expires
// or someone calls session.Dispose()
}
func (r *longLivedRenderKeyProvider) Dispose(ctx context.Context) {
deleteRenderKey(r.cache, r.log, ctx, r.renderKey)
}

@ -58,7 +58,7 @@ func (rs *RenderingService) renderViaHTTP(ctx context.Context, renderKey string,
rendererURL.RawQuery = queryParams.Encode()
// gives service some additional time to timeout and return possible errors.
reqContext, cancel := context.WithTimeout(ctx, opts.Timeout+time.Second*2)
reqContext, cancel := context.WithTimeout(ctx, getRequestTimeout(opts.TimeoutOpts))
defer cancel()
resp, err := rs.doRequest(reqContext, rendererURL, opts.Headers)
@ -103,7 +103,7 @@ func (rs *RenderingService) renderCSVViaHTTP(ctx context.Context, renderKey stri
rendererURL.RawQuery = queryParams.Encode()
// gives service some additional time to timeout and return possible errors.
reqContext, cancel := context.WithTimeout(ctx, opts.Timeout+time.Second*2)
reqContext, cancel := context.WithTimeout(ctx, getRequestTimeout(opts.TimeoutOpts))
defer cancel()
resp, err := rs.doRequest(reqContext, rendererURL, opts.Headers)

@ -26,13 +26,30 @@ const (
ThemeDark Theme = "dark"
)
type TimeoutOpts struct {
Timeout time.Duration // Timeout param passed to image-renderer service
RequestTimeoutMultiplier time.Duration // RequestTimeoutMultiplier used for plugin/HTTP request context timeout
}
type AuthOpts struct {
OrgID int64
UserID int64
OrgRole models.RoleType
}
func getRequestTimeout(opt TimeoutOpts) time.Duration {
if opt.RequestTimeoutMultiplier == 0 {
return opt.Timeout * 2 // default
}
return opt.Timeout * opt.RequestTimeoutMultiplier
}
type Opts struct {
TimeoutOpts
AuthOpts
Width int
Height int
Timeout time.Duration
OrgID int64
UserID int64
OrgRole models.RoleType
Path string
Encoding string
Timezone string
@ -43,10 +60,8 @@ type Opts struct {
}
type CSVOpts struct {
Timeout time.Duration
OrgID int64
UserID int64
OrgRole models.RoleType
TimeoutOpts
AuthOpts
Path string
Encoding string
Timezone string
@ -66,11 +81,27 @@ type RenderCSVResult struct {
type renderFunc func(ctx context.Context, renderKey string, options Opts) (*RenderResult, error)
type renderCSVFunc func(ctx context.Context, renderKey string, options CSVOpts) (*RenderCSVResult, error)
type renderKeyProvider interface {
get(ctx context.Context, opts AuthOpts) (string, error)
afterRequest(ctx context.Context, opts AuthOpts, renderKey string)
}
type SessionOpts struct {
Expiry time.Duration
RefreshExpiryOnEachRequest bool
}
type Session interface {
renderKeyProvider
Dispose(ctx context.Context)
}
type Service interface {
IsAvailable() bool
Version() string
Render(ctx context.Context, opts Opts) (*RenderResult, error)
RenderCSV(ctx context.Context, opts CSVOpts) (*RenderCSVResult, error)
Render(ctx context.Context, opts Opts, session Session) (*RenderResult, error)
RenderCSV(ctx context.Context, opts CSVOpts, session Session) (*RenderCSVResult, error)
RenderErrorImage(theme Theme, error error) (*RenderResult, error)
GetRenderUser(ctx context.Context, key string) (*RenderUser, bool)
CreateRenderingSession(ctx context.Context, authOpts AuthOpts, sessionOpts SessionOpts) (Session, error)
}

@ -4,7 +4,6 @@ import (
"context"
"errors"
"fmt"
"time"
"github.com/grafana/grafana/pkg/plugins/backendplugin/pluginextensionv2"
)
@ -15,7 +14,7 @@ func (rs *RenderingService) startPlugin(ctx context.Context) error {
func (rs *RenderingService) renderViaPlugin(ctx context.Context, renderKey string, opts Opts) (*RenderResult, error) {
// gives plugin some additional time to timeout and return possible errors.
ctx, cancel := context.WithTimeout(ctx, opts.Timeout+time.Second*2)
ctx, cancel := context.WithTimeout(ctx, getRequestTimeout(opts.TimeoutOpts))
defer cancel()
filePath, err := rs.getNewFilePath(RenderPNG)
@ -62,7 +61,7 @@ func (rs *RenderingService) renderViaPlugin(ctx context.Context, renderKey strin
func (rs *RenderingService) renderCSVViaPlugin(ctx context.Context, renderKey string, opts CSVOpts) (*RenderCSVResult, error) {
// gives plugin some additional time to timeout and return possible errors.
ctx, cancel := context.WithTimeout(ctx, opts.Timeout+time.Second*2)
ctx, cancel := context.WithTimeout(ctx, getRequestTimeout(opts.TimeoutOpts))
defer cancel()
filePath, err := rs.getNewFilePath(RenderCSV)

@ -17,7 +17,6 @@ import (
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/metrics"
"github.com/grafana/grafana/pkg/infra/remotecache"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
@ -28,13 +27,6 @@ func init() {
}
const ServiceName = "RenderingService"
const renderKeyPrefix = "render-%s"
type RenderUser struct {
OrgID int64
UserID int64
OrgRole string
}
type RenderingService struct {
log log.Logger
@ -46,9 +38,10 @@ type RenderingService struct {
version string
versionMutex sync.RWMutex
Cfg *setting.Cfg
RemoteCacheService *remotecache.RemoteCache
RendererPluginManager plugins.RendererManager
perRequestRenderKeyProvider renderKeyProvider
Cfg *setting.Cfg
RemoteCacheService *remotecache.RemoteCache
RendererPluginManager plugins.RendererManager
}
func ProvideService(cfg *setting.Cfg, remoteCache *remotecache.RemoteCache, rm plugins.RendererManager) (*RenderingService, error) {
@ -81,11 +74,17 @@ func ProvideService(cfg *setting.Cfg, remoteCache *remotecache.RemoteCache, rm p
domain = "localhost"
}
logger := log.New("rendering")
s := &RenderingService{
perRequestRenderKeyProvider: &perRequestRenderKeyProvider{
cache: remoteCache,
log: logger,
keyExpiry: 5 * time.Minute,
},
Cfg: cfg,
RemoteCacheService: remoteCache,
RendererPluginManager: rm,
log: log.New("rendering"),
log: logger,
domain: domain,
}
return s, nil
@ -195,9 +194,14 @@ func (rs *RenderingService) renderUnavailableImage() *RenderResult {
}
}
func (rs *RenderingService) Render(ctx context.Context, opts Opts) (*RenderResult, error) {
func (rs *RenderingService) Render(ctx context.Context, opts Opts, session Session) (*RenderResult, error) {
startTime := time.Now()
result, err := rs.render(ctx, opts)
renderKeyProvider := rs.perRequestRenderKeyProvider
if session != nil {
renderKeyProvider = session
}
result, err := rs.render(ctx, opts, renderKeyProvider)
elapsedTime := time.Since(startTime).Milliseconds()
saveMetrics(elapsedTime, err, RenderPNG)
@ -205,7 +209,7 @@ func (rs *RenderingService) Render(ctx context.Context, opts Opts) (*RenderResul
return result, err
}
func (rs *RenderingService) render(ctx context.Context, opts Opts) (*RenderResult, error) {
func (rs *RenderingService) render(ctx context.Context, opts Opts, renderKeyProvider renderKeyProvider) (*RenderResult, error) {
if int(atomic.LoadInt32(&rs.inProgressCount)) > opts.ConcurrentLimit {
rs.log.Warn("Could not render image, hit the currency limit", "concurrencyLimit", opts.ConcurrentLimit, "path", opts.Path)
@ -230,12 +234,12 @@ func (rs *RenderingService) render(ctx context.Context, opts Opts) (*RenderResul
if math.IsInf(opts.DeviceScaleFactor, 0) || math.IsNaN(opts.DeviceScaleFactor) || opts.DeviceScaleFactor == 0 {
opts.DeviceScaleFactor = 1
}
renderKey, err := rs.generateAndStoreRenderKey(ctx, opts.OrgID, opts.UserID, opts.OrgRole)
renderKey, err := renderKeyProvider.get(ctx, opts.AuthOpts)
if err != nil {
return nil, err
}
defer rs.deleteRenderKey(ctx, renderKey)
defer renderKeyProvider.afterRequest(ctx, opts.AuthOpts, renderKey)
defer func() {
metrics.MRenderingQueue.Set(float64(atomic.AddInt32(&rs.inProgressCount, -1)))
@ -245,9 +249,14 @@ func (rs *RenderingService) render(ctx context.Context, opts Opts) (*RenderResul
return rs.renderAction(ctx, renderKey, opts)
}
func (rs *RenderingService) RenderCSV(ctx context.Context, opts CSVOpts) (*RenderCSVResult, error) {
func (rs *RenderingService) RenderCSV(ctx context.Context, opts CSVOpts, session Session) (*RenderCSVResult, error) {
startTime := time.Now()
result, err := rs.renderCSV(ctx, opts)
renderKeyProvider := rs.perRequestRenderKeyProvider
if session != nil {
renderKeyProvider = session
}
result, err := rs.renderCSV(ctx, opts, renderKeyProvider)
elapsedTime := time.Since(startTime).Milliseconds()
saveMetrics(elapsedTime, err, RenderCSV)
@ -255,7 +264,7 @@ func (rs *RenderingService) RenderCSV(ctx context.Context, opts CSVOpts) (*Rende
return result, err
}
func (rs *RenderingService) renderCSV(ctx context.Context, opts CSVOpts) (*RenderCSVResult, error) {
func (rs *RenderingService) renderCSV(ctx context.Context, opts CSVOpts, renderKeyProvider renderKeyProvider) (*RenderCSVResult, error) {
if int(atomic.LoadInt32(&rs.inProgressCount)) > opts.ConcurrentLimit {
return nil, ErrConcurrentLimitReached
}
@ -265,12 +274,12 @@ func (rs *RenderingService) renderCSV(ctx context.Context, opts CSVOpts) (*Rende
}
rs.log.Info("Rendering", "path", opts.Path)
renderKey, err := rs.generateAndStoreRenderKey(ctx, opts.OrgID, opts.UserID, opts.OrgRole)
renderKey, err := renderKeyProvider.get(ctx, opts.AuthOpts)
if err != nil {
return nil, err
}
defer rs.deleteRenderKey(ctx, renderKey)
defer renderKeyProvider.afterRequest(ctx, opts.AuthOpts, renderKey)
defer func() {
metrics.MRenderingQueue.Set(float64(atomic.AddInt32(&rs.inProgressCount, -1)))
@ -280,21 +289,6 @@ func (rs *RenderingService) renderCSV(ctx context.Context, opts CSVOpts) (*Rende
return rs.renderCSVAction(ctx, renderKey, opts)
}
func (rs *RenderingService) GetRenderUser(ctx context.Context, key string) (*RenderUser, bool) {
val, err := rs.RemoteCacheService.Get(ctx, fmt.Sprintf(renderKeyPrefix, key))
if err != nil {
rs.log.Error("Failed to get render key from cache", "error", err)
}
if val != nil {
if user, ok := val.(*RenderUser); ok {
return user, true
}
}
return nil, false
}
func (rs *RenderingService) getNewFilePath(rt RenderType) (string, error) {
rand, err := util.GetRandomString(20)
if err != nil {
@ -340,31 +334,6 @@ func (rs *RenderingService) getURL(path string) string {
return fmt.Sprintf("%s://%s:%s%s/%s&render=1", protocol, rs.domain, rs.Cfg.HTTPPort, subPath, path)
}
func (rs *RenderingService) generateAndStoreRenderKey(ctx context.Context, orgId, userId int64, orgRole models.RoleType) (string, error) {
key, err := util.GetRandomString(32)
if err != nil {
return "", err
}
err = rs.RemoteCacheService.Set(ctx, fmt.Sprintf(renderKeyPrefix, key), &RenderUser{
OrgID: orgId,
UserID: userId,
OrgRole: string(orgRole),
}, 5*time.Minute)
if err != nil {
return "", err
}
return key, nil
}
func (rs *RenderingService) deleteRenderKey(ctx context.Context, key string) {
err := rs.RemoteCacheService.Delete(ctx, fmt.Sprintf(renderKeyPrefix, key))
if err != nil {
rs.log.Error("Failed to delete render key", "error", err)
}
}
func isoTimeOffsetToPosixTz(isoOffset string) string {
// invert offset
if strings.HasPrefix(isoOffset, "UTC+") {

@ -138,7 +138,7 @@ func TestRenderLimitImage(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
opts := Opts{Theme: tc.theme, ConcurrentLimit: 1}
result, err := rs.Render(context.Background(), opts)
result, err := rs.Render(context.Background(), opts, nil)
assert.NoError(t, err)
assert.Equal(t, tc.expected, result.FilePath)
})

@ -139,9 +139,15 @@ func (r *simpleCrawler) Start(c *models.ReqContext, mode CrawlerMode, theme rend
r.mode = mode
r.opts = rendering.Opts{
OrgID: c.OrgId,
UserID: c.UserId,
OrgRole: c.OrgRole,
AuthOpts: rendering.AuthOpts{
OrgID: c.OrgId,
UserID: c.UserId,
OrgRole: c.OrgRole,
},
TimeoutOpts: rendering.TimeoutOpts{
Timeout: 10 * time.Second,
RequestTimeoutMultiplier: 3,
},
Theme: theme,
ConcurrentLimit: 10,
}
@ -204,14 +210,11 @@ func (r *simpleCrawler) walk() {
Width: 320,
Height: 240,
Path: panelURL,
OrgID: r.opts.OrgID,
UserID: r.opts.UserID,
ConcurrentLimit: r.opts.ConcurrentLimit,
OrgRole: r.opts.OrgRole,
AuthOpts: r.opts.AuthOpts,
TimeoutOpts: r.opts.TimeoutOpts,
Theme: r.opts.Theme,
Timeout: 10 * time.Second,
DeviceScaleFactor: -5, // negative numbers will render larger then scale down
})
}, nil)
if err != nil {
tlog.Warn("error getting image", "err", err)
r.status.Errors++

Loading…
Cancel
Save