Skip to content

Commit 48a86ac

Browse files
committed
add timeout; return no results if no results were found
1 parent 3ff608c commit 48a86ac

File tree

4 files changed

+75
-15
lines changed

4 files changed

+75
-15
lines changed

datasource.go

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,27 @@ package sqlds
33
import (
44
"context"
55
"database/sql"
6+
"errors"
7+
"fmt"
68
"net/http"
79
"sync"
10+
"time"
811

912
"github.com/grafana/grafana-plugin-sdk-go/backend"
1013
"github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt"
1114
"github.com/grafana/grafana-plugin-sdk-go/backend/resource/httpadapter"
1215
"github.com/grafana/grafana-plugin-sdk-go/data"
13-
"github.com/pkg/errors"
1416
)
1517

1618
type sqldatasource struct {
19+
Completable
20+
1721
db *sql.DB
1822
c Driver
1923
settings backend.DataSourceInstanceSettings
24+
timeout time.Duration
2025

2126
backend.CallResourceHandler
22-
Completable
2327
CustomRoutes map[string]func(http.ResponseWriter, *http.Request)
2428
}
2529

@@ -37,7 +41,9 @@ func (ds *sqldatasource) NewDatasource(settings backend.DataSourceInstanceSettin
3741
if err != nil {
3842
return nil, err
3943
}
44+
4045
ds.CallResourceHandler = httpadapter.New(mux)
46+
ds.timeout = ds.c.Timeout(settings)
4147

4248
return ds, nil
4349
}
@@ -66,7 +72,7 @@ func (ds *sqldatasource) QueryData(ctx context.Context, req *backend.QueryDataRe
6672
// Execute each query and store the results by query RefID
6773
for _, q := range req.Queries {
6874
go func(query backend.DataQuery) {
69-
frames, err := ds.handleQuery(query)
75+
frames, err := ds.handleQuery(ctx, query)
7076

7177
response.Set(query.RefID, backend.DataResponse{
7278
Frames: frames,
@@ -83,7 +89,7 @@ func (ds *sqldatasource) QueryData(ctx context.Context, req *backend.QueryDataRe
8389
}
8490

8591
// handleQuery will call query, and attempt to reconnect if the query failed
86-
func (ds *sqldatasource) handleQuery(req backend.DataQuery) (data.Frames, error) {
92+
func (ds *sqldatasource) handleQuery(ctx context.Context, req backend.DataQuery) (data.Frames, error) {
8793
// Convert the backend.DataQuery into a Query object
8894
q, err := GetQuery(req)
8995
if err != nil {
@@ -93,7 +99,7 @@ func (ds *sqldatasource) handleQuery(req backend.DataQuery) (data.Frames, error)
9399
// Apply supported macros to the query
94100
q.RawSQL, err = interpolate(ds.c, q)
95101
if err != nil {
96-
return nil, errors.WithMessage(err, "Could not apply macros")
102+
return nil, fmt.Errorf("%s: %w", "Could not apply macros", err)
97103
}
98104

99105
// Apply the default FillMode, overwritting it if the query specifies it
@@ -102,21 +108,35 @@ func (ds *sqldatasource) handleQuery(req backend.DataQuery) (data.Frames, error)
102108
fillMode = q.FillMissing
103109
}
104110

111+
if ds.timeout != 0 {
112+
tctx, cancel := context.WithTimeout(ctx, ds.timeout)
113+
defer cancel()
114+
115+
ctx = tctx
116+
}
117+
105118
// FIXES:
106119
// * Some datasources (snowflake) expire connections or have an authentication token that expires if not used in 1 or 4 hours.
107120
// Because the datasource driver does not include an option for permanent connections, we retry the connection
108121
// if the query fails. NOTE: this does not include some errors like "ErrNoRows"
109-
res, err := query(ds.db, ds.c.Converters(), fillMode, q)
122+
123+
// This function will return an "ErrorTimeout" if the context's done channel receives data
124+
res, err := queryContext(ctx, ds.db, ds.c.Converters(), fillMode, q)
110125
if err == nil {
111126
return res, nil
112127
}
113128

114-
if errors.Cause(err) == ErrorQuery {
129+
if errors.Is(err, ErrorNoResults) {
130+
return nil, nil
131+
}
132+
133+
if errors.Is(err, ErrorQuery) {
115134
ds.db, err = ds.c.Connect(ds.settings)
116135
if err != nil {
117136
return nil, err
118137
}
119-
return query(ds.db, ds.c.Converters(), fillMode, q)
138+
139+
return queryContext(ctx, ds.db, ds.c.Converters(), fillMode, q)
120140
}
121141

122142
return nil, err

driver.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package sqlds
22

33
import (
44
"database/sql"
5+
"time"
56

67
"github.com/grafana/grafana-plugin-sdk-go/backend"
78
"github.com/grafana/grafana-plugin-sdk-go/data"
@@ -13,7 +14,9 @@ import (
1314
type Driver interface {
1415
// Connect connects to the database. It does not need to call `db.Ping()`
1516
Connect(backend.DataSourceInstanceSettings) (*sql.DB, error)
17+
Timeout(backend.DataSourceInstanceSettings) time.Duration
1618
FillMode() *data.FillMissing
1719
Macros() Macros
1820
Converters() []sqlutil.Converter
21+
// Timeout is used when sending a query to the backing data source. If the timeout is exceeded, the QueryData function will return an error.
1922
}

errors.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,8 @@ var (
99
ErrorJSON = errors.New("error unmarshaling query JSON the Query Model")
1010
// ErrorQuery is returned when the query could not complete / execute
1111
ErrorQuery = errors.New("error querying the database")
12+
// ErrorTimeout is returned if the query has timed out
13+
ErrorTimeout = errors.New("query timeout exceeded")
14+
// ErrorNoResults is returned if there were no results returned
15+
ErrorNoResults = errors.New("no results returned from query")
1216
)

query.go

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
package sqlds
22

33
import (
4+
"context"
45
"database/sql"
56
"encoding/json"
7+
"fmt"
68
"time"
79

810
"github.com/grafana/grafana-plugin-sdk-go/backend"
911
"github.com/grafana/grafana-plugin-sdk-go/data"
1012
"github.com/grafana/grafana-plugin-sdk-go/data/sqlutil"
11-
"github.com/pkg/errors"
1213
)
1314

1415
// FormatQueryOption defines how the user has chosen to represent the data
@@ -90,22 +91,44 @@ func getErrorFrameFromQuery(query *Query) data.Frames {
9091
return frames
9192
}
9293

94+
func queryContext(ctx context.Context, db *sql.DB, converters []sqlutil.Converter, fillMode *data.FillMissing, q *Query) (data.Frames, error) {
95+
var (
96+
done = make(chan bool)
97+
res = data.Frames{}
98+
err error
99+
)
100+
101+
go func() {
102+
res, err = query(ctx, db, converters, fillMode, q)
103+
done <- true
104+
}()
105+
106+
select {
107+
case <-done:
108+
// The done channel will receive data if the query funciton has completed
109+
return res, err
110+
case <-ctx.Done():
111+
// The context's done channel will receive data if the timeout is exceeded
112+
return nil, ErrorTimeout
113+
}
114+
}
115+
93116
// query sends the query to the sql.DB and converts the rows to a dataframe.
94-
func query(db *sql.DB, converters []sqlutil.Converter, fillMode *data.FillMissing, query *Query) (data.Frames, error) {
117+
func query(ctx context.Context, db *sql.DB, converters []sqlutil.Converter, fillMode *data.FillMissing, query *Query) (data.Frames, error) {
95118
// Query the rows from the database
96-
rows, err := db.Query(query.RawSQL)
119+
rows, err := db.QueryContext(ctx, query.RawSQL)
97120
if err != nil {
98-
return getErrorFrameFromQuery(query), errors.Wrap(ErrorQuery, err.Error())
121+
return getErrorFrameFromQuery(query), fmt.Errorf("%w: %s", ErrorQuery, err.Error())
99122
}
100123

101124
// Check for an error response
102125
if err := rows.Err(); err != nil {
103126
if err == sql.ErrNoRows {
104127
// Should we even response with an error here?
105128
// The panel will simply show "no data"
106-
return getErrorFrameFromQuery(query), errors.WithMessage(err, "No results from query")
129+
return getErrorFrameFromQuery(query), fmt.Errorf("%s: %w", "No results from query", err)
107130
}
108-
return getErrorFrameFromQuery(query), errors.WithMessage(err, "Error response from database")
131+
return getErrorFrameFromQuery(query), fmt.Errorf("%s: %w", "Error response from database", err)
109132
}
110133

111134
defer func() {
@@ -117,7 +140,7 @@ func query(db *sql.DB, converters []sqlutil.Converter, fillMode *data.FillMissin
117140
// Convert the response to frames
118141
res, err := getFrames(rows, -1, converters, fillMode, query)
119142
if err != nil {
120-
return nil, errors.WithMessage(err, "Could not process SQL results")
143+
return nil, fmt.Errorf("%w: %s", err, "Could not process SQL results")
121144
}
122145

123146
return res, nil
@@ -139,6 +162,16 @@ func getFrames(rows *sql.Rows, limit int64, converters []sqlutil.Converter, fill
139162
return data.Frames{frame}, nil
140163
}
141164

165+
count, err := frame.RowLen()
166+
167+
if err != nil {
168+
return nil, err
169+
}
170+
171+
if count == 0 {
172+
return nil, ErrorNoResults
173+
}
174+
142175
if frame.TimeSeriesSchema().Type == data.TimeSeriesTypeLong {
143176
frame, err := data.LongToWide(frame, fillMode)
144177
if err != nil {

0 commit comments

Comments
 (0)