Introduce query timeout configuration (#2157)

## Summary

Implements configurable query execution timeout controls to prevent
poorly optimized or excessive queries from consuming excessive server
resources, causing performance degradation, or crashing the Zabbix
server.

Fixes: https://github.com/grafana/oss-big-tent-squad/issues/127

## Problem

Previously, the plugin only had an HTTP connection timeout (`timeout`)
that controlled individual API request timeouts. However, a complete
query execution could involve multiple API calls and run indefinitely if
not properly controlled, potentially causing resource exhaustion.

## Solution

Added a new `queryTimeout` setting that enforces a maximum execution
time for entire database queries initiated by the plugin. Queries
exceeding this limit are automatically terminated with proper error
handling and logging.

## Testing

1. Configure a datasource with `queryTimeout` set to a low value (e.g.,
5 seconds)
2. Execute a query that would normally take longer than the timeout
3. Verify that:
   - Query is terminated after the timeout period
   - Error message indicates timeout occurred
   - Logs contain timeout warning with query details
   - Other queries in the same request continue to execute

## Notes

- `queryTimeout` is separate from `timeout` (HTTP connection timeout)
- `queryTimeout` applies to the entire query execution, which may
involve multiple API calls
- Default value of 60 seconds ensures reasonable protection while
allowing normal queries to complete
- Timeout errors are logged with query refId, queryType, timeout
duration, and datasourceId for troubleshooting
This commit is contained in:
ismail simsek
2026-01-12 15:30:31 +01:00
committed by GitHub
parent 7eb80d3f23
commit a2f8b6433a
7 changed files with 366 additions and 50 deletions

View File

@@ -11,6 +11,31 @@ import (
"github.com/grafana/grafana-plugin-sdk-go/backend"
)
// parseTimeoutValue parses a timeout value from various types (string, float64, int64, int)
// and returns it as int64. If the value is empty or invalid, it returns the default value.
// The fieldName parameter is used for error messages.
func parseTimeoutValue(value interface{}, defaultValue int64, fieldName string) (int64, error) {
switch t := value.(type) {
case string:
if t == "" {
return defaultValue, nil
}
timeoutInt, err := strconv.Atoi(t)
if err != nil {
return 0, errors.New("failed to parse " + fieldName + ": " + err.Error())
}
return int64(timeoutInt), nil
case float64:
return int64(t), nil
case int64:
return t, nil
case int:
return int64(t), nil
default:
return defaultValue, nil
}
}
func ReadZabbixSettings(dsInstanceSettings *backend.DataSourceInstanceSettings) (*ZabbixDatasourceSettings, error) {
zabbixSettingsDTO := &ZabbixDatasourceSettingsDTO{}
@@ -33,10 +58,6 @@ func ReadZabbixSettings(dsInstanceSettings *backend.DataSourceInstanceSettings)
zabbixSettingsDTO.CacheTTL = "1h"
}
//if zabbixSettingsDTO.Timeout == 0 {
// zabbixSettingsDTO.Timeout = 30
//}
trendsFrom, err := gtime.ParseInterval(zabbixSettingsDTO.TrendsFrom)
if err != nil {
return nil, err
@@ -52,22 +73,19 @@ func ReadZabbixSettings(dsInstanceSettings *backend.DataSourceInstanceSettings)
return nil, err
}
var timeout int64
switch t := zabbixSettingsDTO.Timeout.(type) {
case string:
if t == "" {
timeout = 30
break
}
timeoutInt, err := strconv.Atoi(t)
if err != nil {
return nil, errors.New("failed to parse timeout: " + err.Error())
}
timeout = int64(timeoutInt)
case float64:
timeout = int64(t)
default:
timeout = 30
timeout, err := parseTimeoutValue(zabbixSettingsDTO.Timeout, 30, "timeout")
if err != nil {
return nil, err
}
queryTimeout, err := parseTimeoutValue(zabbixSettingsDTO.QueryTimeout, 60, "queryTimeout")
if err != nil {
return nil, err
}
// Default to 60 seconds if queryTimeout is 0 or negative
if queryTimeout <= 0 {
queryTimeout = 60
}
zabbixSettings := &ZabbixDatasourceSettings{
@@ -77,6 +95,7 @@ func ReadZabbixSettings(dsInstanceSettings *backend.DataSourceInstanceSettings)
TrendsRange: trendsRange,
CacheTTL: cacheTTL,
Timeout: time.Duration(timeout) * time.Second,
QueryTimeout: time.Duration(queryTimeout) * time.Second,
DisableDataAlignment: zabbixSettingsDTO.DisableDataAlignment,
DisableReadOnlyUsersAck: zabbixSettingsDTO.DisableReadOnlyUsersAck,
}