Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Autoinstrumentation improvements #1066

Merged
merged 5 commits into from
Sep 4, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 21 additions & 45 deletions backend/data_adapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,78 +29,54 @@ func (a *dataSDKAdapter) QueryData(ctx context.Context, req *pluginv2.QueryDataR
var innerErr error
resp, innerErr = a.queryDataHandler.QueryData(ctx, parsedReq)

if resp == nil || len(resp.Responses) == 0 {
return RequestStatusFromError(innerErr), innerErr
}

if isCancelledError(innerErr) {
return RequestStatusCancelled, nil
}

if isHTTPTimeoutError(innerErr) {
return RequestStatusError, nil
}
status := RequestStatusFromQueryDataResponse(resp, innerErr)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is iterating the data responses and we do that below as well, but feels like much cleaner code doing it this way

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like that check if resp == nil || len(resp.Responses) == 0 { was needed also for code bellow, because now, if we return nil response, we get panic in for refID, r := range resp.Responses code
image

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's correct, but I still get a separate panic even after adding back the same if resp == nil || len(resp.Responses) == 0 { check.

This time the panic is here:

return ToProto().QueryDataResponse(resp)

Responses: make(map[string]*pluginv2.DataResponse, len(res.Responses)),

I also get it on the latest main:

diff --git a/backend/data_adapter_test.go b/backend/data_adapter_test.go
index 57fd761..70423bb 100644
--- a/backend/data_adapter_test.go
+++ b/backend/data_adapter_test.go
@@ -180,6 +180,11 @@ func TestQueryData(t *testing.T) {
                                },
                                expErrorSource: ErrorSourcePlugin,
                        },
+                       {
+                               name:              "nil queryDataResponse",
+                               queryDataResponse: nil,
+                               expErrorSource:    ErrorSourcePlugin,
+                       },
                } {
                        t.Run(tc.name, func(t *testing.T) {
                                var actualCtx context.Context
--- FAIL: TestQueryData/TestQueryDataResponse/nil_queryDataResponse (0.00s)

--- FAIL: TestQueryData/TestQueryDataResponse (0.00s)

--- FAIL: TestQueryData (0.00s)
panic: runtime error: invalid memory address or nil pointer dereference [recovered]
	panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0xe6a4c5]

goroutine 73 [running]:
testing.tRunner.func1.2({0xf8e600, 0x1aa5d60})
	/usr/local/go/src/testing/testing.go:1632 +0x230
testing.tRunner.func1()
	/usr/local/go/src/testing/testing.go:1635 +0x35e
panic({0xf8e600?, 0x1aa5d60?})
	/usr/local/go/src/runtime/panic.go:785 +0x132
github.com/grafana/grafana-plugin-sdk-go/backend.ConvertToProtobuf.QueryDataResponse({}, 0x0)
	/home/giuseppe/grafana/grafana-plugin-sdk-go/backend/convert_to_protobuf.go:197 +0x25
github.com/grafana/grafana-plugin-sdk-go/backend.(*dataSDKAdapter).QueryData(0xc000474620, {0x1298120?, 0x1af9460?}, 0xc00011c700)
	/home/giuseppe/grafana/grafana-plugin-sdk-go/backend/data_adapter.go:109 +0x16e
github.com/grafana/grafana-plugin-sdk-go/backend.TestQueryData.func4.1(0xc000190820)
	/home/giuseppe/grafana/grafana-plugin-sdk-go/backend/data_adapter_test.go:195 +0x12a
testing.tRunner(0xc000190820, 0xc0004742d0)
	/usr/local/go/src/testing/testing.go:1690 +0xf4
created by testing.(*T).Run in goroutine 67
	/usr/local/go/src/testing/testing.go:1743 +0x390


Process finished with the exit code 1

So this section needs better error handling in general :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you returning both a nil response and a nil error in your datasource?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can see the test doing that, but was referring to your datasource @ivanahuckova

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you returning both a nil response and a nil error in your datasource?

No, I was returning nil response and error. You can see it in the screenshot as well.
image

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pushed some changes:

  • if error is not nil returns early
  • If both error and resp is nil an error will be returned

ctxLogger := Logger.FromContext(ctx)

// Set downstream status source in the context if there's at least one response with downstream status source,
// and if there's no plugin error
var hasPluginError bool
var hasDownstreamError bool
var hasCancelledError bool
var hasHTTPTimeoutError bool
for _, r := range resp.Responses {
var hasPluginError, hasDownstreamError bool
for refID, r := range resp.Responses {
if r.Error == nil {
continue
}

if isCancelledError(r.Error) {
hasCancelledError = true
// if error source not set and the error is a downstream error, set error source to downstream.
if !r.ErrorSource.IsValid() && IsDownstreamError(r.Error) {
r.ErrorSource = ErrorSourceDownstream
}
if isHTTPTimeoutError(r.Error) {
hasHTTPTimeoutError = true

if !r.Status.IsValid() {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one I copied from

if !status.IsValid() {
status = statusFromError(dr.Error)
}

r.Status = statusFromError(r.Error)
}

if r.ErrorSource == ErrorSourceDownstream {
hasDownstreamError = true
} else {
hasPluginError = true
}
}

if hasCancelledError {
if err := WithDownstreamErrorSource(ctx); err != nil {
return RequestStatusError, fmt.Errorf("failed to set downstream status source: %w", errors.Join(innerErr, err))
}
return RequestStatusCancelled, nil
}

if hasHTTPTimeoutError {
if err := WithDownstreamErrorSource(ctx); err != nil {
return RequestStatusError, fmt.Errorf("failed to set downstream status source: %w", errors.Join(innerErr, err))
logParams := []any{
"refID", refID,
"status", int(r.Status),
"error", r.Error,
"statusSource", string(r.ErrorSource),
}
return RequestStatusError, nil
ctxLogger.Error("Partial data response error", logParams...)
}

// A plugin error has higher priority than a downstream error,
// so set to downstream only if there's no plugin error
if hasDownstreamError && !hasPluginError {
if err := WithDownstreamErrorSource(ctx); err != nil {
return RequestStatusError, fmt.Errorf("failed to set downstream status source: %w", errors.Join(innerErr, err))
}
return RequestStatusError, nil
}

if hasPluginError {
if err := WithErrorSource(ctx, ErrorSourcePlugin); err != nil {
return RequestStatusError, fmt.Errorf("failed to set plugin status source: %w", errors.Join(innerErr, err))
}
return RequestStatusError, nil
}

if innerErr != nil {
return RequestStatusFromError(innerErr), innerErr
} else if hasDownstreamError {
if err := WithDownstreamErrorSource(ctx); err != nil {
return RequestStatusError, fmt.Errorf("failed to set downstream status source: %w", errors.Join(innerErr, err))
}
}

return RequestStatusOK, nil
return status, nil
})
if err != nil {
return nil, err
Expand Down
20 changes: 20 additions & 0 deletions backend/data_adapter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,26 @@ func TestQueryData(t *testing.T) {
},
expErrorSource: ErrorSourcePlugin,
},
{
name: `single downstream error without error source should be "downstream" error source`,
queryDataResponse: &QueryDataResponse{
Responses: map[string]DataResponse{
"A": {Error: DownstreamErrorf("boom")},
},
},
expErrorSource: ErrorSourceDownstream,
},
{
name: `multiple downstream error without error source and single plugin error should be "plugin" error source`,
queryDataResponse: &QueryDataResponse{
Responses: map[string]DataResponse{
"A": {Error: DownstreamErrorf("boom")},
"B": {Error: someErr},
"C": {Error: DownstreamErrorf("boom")},
},
},
expErrorSource: ErrorSourcePlugin,
},
} {
t.Run(tc.name, func(t *testing.T) {
var actualCtx context.Context
Expand Down
6 changes: 5 additions & 1 deletion backend/error_source.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ const (
DefaultErrorSource ErrorSource = ErrorSourcePlugin
)

func (es ErrorSource) IsValid() bool {
return es == ErrorSourceDownstream || es == ErrorSourcePlugin
}

// ErrorSourceFromStatus returns an [ErrorSource] based on provided HTTP status code.
func ErrorSourceFromHTTPStatus(statusCode int) ErrorSource {
switch statusCode {
Expand Down Expand Up @@ -62,7 +66,7 @@ func IsDownstreamError(err error) bool {
return true
}

if isHTTPTimeoutError(err) {
if isHTTPTimeoutError(err) || isCancelledError(err) {
return true
}

Expand Down
41 changes: 41 additions & 0 deletions backend/error_source_test.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,26 @@
package backend

import (
"context"
"errors"
"fmt"
"net"
"os"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)

func TestErrorSource(t *testing.T) {
var es ErrorSource
require.False(t, es.IsValid())
require.True(t, ErrorSourceDownstream.IsValid())
require.True(t, ErrorSourcePlugin.IsValid())
}

func TestIsDownstreamError(t *testing.T) {
tcs := []struct {
name string
Expand Down Expand Up @@ -66,6 +77,36 @@ func TestIsDownstreamError(t *testing.T) {
err: fmt.Errorf("other error"),
expected: false,
},
{
name: "context.Canceled",
err: context.Canceled,
expected: true,
},
{
name: "wrapped context.Canceled",
err: fmt.Errorf("error: %w", context.Canceled),
expected: true,
},
{
name: "joined context.Canceled",
err: errors.Join(fmt.Errorf("oh no"), context.Canceled),
expected: true,
},
{
name: "gRPC canceled error",
err: status.Error(codes.Canceled, "canceled"),
expected: true,
},
{
name: "wrapped gRPC canceled error",
err: fmt.Errorf("error: %w", status.Error(codes.Canceled, "canceled")),
expected: true,
},
{
name: "joined gRPC canceled error",
err: errors.Join(fmt.Errorf("oh no"), status.Error(codes.Canceled, "canceled")),
expected: true,
},
}
for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) {
Expand Down
2 changes: 1 addition & 1 deletion experimental/apis/data/v0alpha1/openapi_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,5 +42,5 @@ func TestOpenAPI(t *testing.T) {
// Ensure DataSourceRef exists and has three properties
def, ok = defs["github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1.DataSourceRef"]
require.True(t, ok)
require.Equal(t, []string{"type", "uid", "apiVersion"}, maps.Keys(def.Schema.Properties))
require.ElementsMatch(t, []string{"type", "uid", "apiVersion"}, maps.Keys(def.Schema.Properties))
}