mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-08-22 00:42:47 +08:00
🐛 fix(query): 放开所有数据源默认查询超时
- 区分连接超时与查询超时,默认查询仅保留手动取消能力 - 清理各数据库及 HTTP 数据源由连接配置派生的请求超时 - 新增按请求 queryTimeout 透传并保留计数任务显式时限 - 补充前后端、缓存键与多驱动超时策略回归测试
This commit is contained in:
@@ -467,16 +467,20 @@ func chromaAuthHeaders(config connection.ConnectionConfig) map[string]string {
|
||||
|
||||
func buildChromaHTTPClient(config connection.ConnectionConfig) *http.Client {
|
||||
transport := http.DefaultTransport.(*http.Transport).Clone()
|
||||
dialTimeout := getConnectTimeout(config)
|
||||
transport.DialContext = (&net.Dialer{Timeout: dialTimeout, KeepAlive: 30 * time.Second}).DialContext
|
||||
if tlsConfig, err := resolveGenericTLSConfig(config); err == nil && tlsConfig != nil {
|
||||
transport.TLSClientConfig = tlsConfig
|
||||
}
|
||||
if config.UseProxy {
|
||||
proxyCfg := config.Proxy
|
||||
transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
return proxytunnel.DialContext(ctx, proxyCfg, network, addr)
|
||||
dialCtx, cancel := context.WithTimeout(ctx, dialTimeout)
|
||||
defer cancel()
|
||||
return proxytunnel.DialContext(dialCtx, proxyCfg, network, addr)
|
||||
}
|
||||
}
|
||||
return &http.Client{Transport: transport, Timeout: getConnectTimeout(config)}
|
||||
return &http.Client{Transport: transport}
|
||||
}
|
||||
|
||||
func (c *ChromaDB) detectVersion(ctx context.Context) error {
|
||||
|
||||
@@ -32,8 +32,10 @@ const (
|
||||
defaultClickHousePort = 9000
|
||||
defaultClickHouseUser = "default"
|
||||
defaultClickHouseDatabase = "default"
|
||||
minClickHouseReadTimeout = 5 * time.Minute
|
||||
clickHouseHTTPPortHint = "8123/8125/8132/8443"
|
||||
// clickhouse-go replaces a zero ReadTimeout with five minutes. Max duration
|
||||
// keeps context cancellation as the only practical automatic query deadline.
|
||||
clickHouseNoAutomaticReadTimeout = time.Duration(1<<63 - 1)
|
||||
clickHouseHTTPPortHint = "8123/8125/8132/8443"
|
||||
|
||||
clickHouseProtocolAuto = "auto"
|
||||
clickHouseProtocolHTTP = "http"
|
||||
@@ -179,10 +181,6 @@ func (c *ClickHouseDB) buildClickHouseOptions(config connection.ConnectionConfig
|
||||
|
||||
func (c *ClickHouseDB) buildClickHouseOptionsWithHTTPCompatibility(config connection.ConnectionConfig, stripHTTPClientProtocolVersion bool) (*clickhouse.Options, error) {
|
||||
connectTimeout := getConnectTimeout(config)
|
||||
readTimeout := connectTimeout
|
||||
if readTimeout < minClickHouseReadTimeout {
|
||||
readTimeout = minClickHouseReadTimeout
|
||||
}
|
||||
protocol := detectClickHouseProtocol(config)
|
||||
opts := &clickhouse.Options{
|
||||
Protocol: protocol,
|
||||
@@ -195,7 +193,7 @@ func (c *ClickHouseDB) buildClickHouseOptionsWithHTTPCompatibility(config connec
|
||||
Password: config.Password,
|
||||
},
|
||||
DialTimeout: connectTimeout,
|
||||
ReadTimeout: readTimeout,
|
||||
ReadTimeout: clickHouseNoAutomaticReadTimeout,
|
||||
}
|
||||
tlsConfig, err := resolveGenericTLSConfig(config)
|
||||
if err != nil {
|
||||
|
||||
@@ -642,7 +642,7 @@ func TestClickHouseOptions_UsesStructuredTimeoutAndAuth(t *testing.T) {
|
||||
if opts.DialTimeout != 15*time.Second {
|
||||
t.Fatalf("dial timeout 不符合预期:%s", opts.DialTimeout)
|
||||
}
|
||||
if opts.ReadTimeout != minClickHouseReadTimeout {
|
||||
if opts.ReadTimeout != clickHouseNoAutomaticReadTimeout {
|
||||
t.Fatalf("read timeout 不符合预期:%s", opts.ReadTimeout)
|
||||
}
|
||||
if _, ok := opts.Settings["write_timeout"]; ok {
|
||||
@@ -687,7 +687,7 @@ func TestClickHouseOptions_MergesConnectionParamsIntoOptionsAndSettings(t *testi
|
||||
}
|
||||
}
|
||||
|
||||
func TestClickHouseOptions_ReadTimeoutUsesLargerConfiguredTimeout(t *testing.T) {
|
||||
func TestClickHouseOptions_ConnectionTimeoutDoesNotBecomeReadTimeout(t *testing.T) {
|
||||
c := &ClickHouseDB{}
|
||||
cfg := normalizeClickHouseConfig(connection.ConnectionConfig{
|
||||
Type: "clickhouse",
|
||||
@@ -709,7 +709,7 @@ func TestClickHouseOptions_ReadTimeoutUsesLargerConfiguredTimeout(t *testing.T)
|
||||
if opts.DialTimeout != 900*time.Second {
|
||||
t.Fatalf("dial timeout 不符合预期:%s", opts.DialTimeout)
|
||||
}
|
||||
if opts.ReadTimeout != 900*time.Second {
|
||||
if opts.ReadTimeout != clickHouseNoAutomaticReadTimeout {
|
||||
t.Fatalf("read timeout 不符合预期:%s", opts.ReadTimeout)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -678,26 +678,23 @@ func buildESClientConfig(config connection.ConnectionConfig) elasticsearch.Confi
|
||||
}
|
||||
}
|
||||
|
||||
// 代理支持
|
||||
if config.UseProxy {
|
||||
transport, ok := cfg.Transport.(*http.Transport)
|
||||
if !ok {
|
||||
transport = http.DefaultTransport.(*http.Transport).Clone()
|
||||
}
|
||||
proxyCfg := config.Proxy
|
||||
transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
return proxytunnel.DialContext(ctx, proxyCfg, network, addr)
|
||||
}
|
||||
cfg.Transport = transport
|
||||
}
|
||||
|
||||
// 超时设置
|
||||
// Keep the connection dial bounded, but let the request context own query
|
||||
// cancellation. A connection timeout must not become a response deadline.
|
||||
timeout := getConnectTimeout(config)
|
||||
if cfg.Transport == nil {
|
||||
cfg.Transport = http.DefaultTransport.(*http.Transport).Clone()
|
||||
}
|
||||
if transport, ok := cfg.Transport.(*http.Transport); ok {
|
||||
transport.ResponseHeaderTimeout = timeout
|
||||
transport.DialContext = (&net.Dialer{Timeout: timeout, KeepAlive: 30 * time.Second}).DialContext
|
||||
if config.UseProxy {
|
||||
proxyCfg := config.Proxy
|
||||
transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
dialCtx, cancel := context.WithTimeout(ctx, timeout)
|
||||
defer cancel()
|
||||
return proxytunnel.DialContext(dialCtx, proxyCfg, network, addr)
|
||||
}
|
||||
}
|
||||
transport.ResponseHeaderTimeout = 0
|
||||
}
|
||||
|
||||
// 包装 transport:注入 X-Elastic-Product 头以兼容 ES 6.x / 7.x 早期版本。
|
||||
|
||||
@@ -59,6 +59,29 @@ func newTestESDB(t *testing.T, serverURL, defaultIndex string) *ElasticsearchDB
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildESClientConfigSeparatesConnectionAndRequestTimeout(t *testing.T) {
|
||||
config := buildESClientConfig(connection.ConnectionConfig{
|
||||
Type: "elasticsearch",
|
||||
Host: "127.0.0.1",
|
||||
Port: defaultEsPort,
|
||||
Timeout: 1,
|
||||
})
|
||||
wrapped, ok := config.Transport.(*esProductCheckBypassTransport)
|
||||
if !ok {
|
||||
t.Fatalf("expected product-check transport wrapper, got %T", config.Transport)
|
||||
}
|
||||
transport, ok := wrapped.inner.(*http.Transport)
|
||||
if !ok {
|
||||
t.Fatalf("expected HTTP transport, got %T", wrapped.inner)
|
||||
}
|
||||
if transport.ResponseHeaderTimeout != 0 {
|
||||
t.Fatalf("connection timeout leaked into Elasticsearch response timeout: %s", transport.ResponseHeaderTimeout)
|
||||
}
|
||||
if transport.DialContext == nil {
|
||||
t.Fatal("expected bounded connection dial")
|
||||
}
|
||||
}
|
||||
|
||||
// buildMockESMappingResponse 构造模拟的 mapping 响应 JSON。
|
||||
func buildMockESMappingResponse(indexName string, fields map[string]string) map[string]interface{} {
|
||||
properties := make(map[string]interface{})
|
||||
|
||||
@@ -201,8 +201,11 @@ func (i *IoTDBDB) QueryContext(ctx context.Context, query string) ([]map[string]
|
||||
if text == "" {
|
||||
return nil, nil, fmt.Errorf("查询语句不能为空")
|
||||
}
|
||||
timeoutMs := int64(i.effectiveTimeout().Milliseconds())
|
||||
ds, err := i.session.Query(ctx, text, &timeoutMs)
|
||||
var timeoutMs *int64
|
||||
if remaining := timeoutMsFromContext(ctx); remaining > 0 {
|
||||
timeoutMs = &remaining
|
||||
}
|
||||
ds, err := i.session.Query(ctx, text, timeoutMs)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"GoNavi-Wails/internal/connection"
|
||||
|
||||
@@ -16,13 +17,19 @@ import (
|
||||
)
|
||||
|
||||
type fakeIoTDBSession struct {
|
||||
queryResults map[string][]map[string]interface{}
|
||||
execs []string
|
||||
queryResults map[string][]map[string]interface{}
|
||||
execs []string
|
||||
queryTimeouts []int64
|
||||
}
|
||||
|
||||
func (f *fakeIoTDBSession) Close() error { return nil }
|
||||
|
||||
func (f *fakeIoTDBSession) Query(_ context.Context, sql string, _ *int64) (iotdbDataSet, error) {
|
||||
func (f *fakeIoTDBSession) Query(_ context.Context, sql string, timeoutMs *int64) (iotdbDataSet, error) {
|
||||
timeout := int64(-1)
|
||||
if timeoutMs != nil {
|
||||
timeout = *timeoutMs
|
||||
}
|
||||
f.queryTimeouts = append(f.queryTimeouts, timeout)
|
||||
rows := f.queryResults[sql]
|
||||
return &fakeIoTDBDataSet{rows: rows, columns: fakeIoTDBColumns(rows)}, nil
|
||||
}
|
||||
@@ -207,6 +214,32 @@ func TestNormalizeIoTDBValueConvertsBinaryText(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestIoTDBQueryContextOnlySendsExplicitDeadlineToServer(t *testing.T) {
|
||||
session := &fakeIoTDBSession{queryResults: map[string][]map[string]interface{}{
|
||||
"SELECT * FROM root.sg.d1": {},
|
||||
}}
|
||||
client := &IoTDBDB{session: session, pingTimeout: time.Second}
|
||||
|
||||
if _, _, err := client.QueryContext(context.Background(), "SELECT * FROM root.sg.d1"); err != nil {
|
||||
t.Fatalf("QueryContext without deadline: %v", err)
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
defer cancel()
|
||||
if _, _, err := client.QueryContext(ctx, "SELECT * FROM root.sg.d1"); err != nil {
|
||||
t.Fatalf("QueryContext with deadline: %v", err)
|
||||
}
|
||||
|
||||
if len(session.queryTimeouts) != 2 {
|
||||
t.Fatalf("query timeout calls = %v", session.queryTimeouts)
|
||||
}
|
||||
if session.queryTimeouts[0] != -1 {
|
||||
t.Fatalf("connection timeout leaked into IoTDB query timeout: %dms", session.queryTimeouts[0])
|
||||
}
|
||||
if session.queryTimeouts[1] <= 0 || session.queryTimeouts[1] > 2000 {
|
||||
t.Fatalf("explicit context deadline was not propagated: %dms", session.queryTimeouts[1])
|
||||
}
|
||||
}
|
||||
|
||||
func TestIoTDBLiveSmoke(t *testing.T) {
|
||||
addr := strings.TrimSpace(os.Getenv("GONAVI_IOTDB_TEST_ADDR"))
|
||||
if addr == "" {
|
||||
|
||||
@@ -705,8 +705,10 @@ func newKafkaGoRuntime(config connection.ConnectionConfig) (kafkaRuntime, error)
|
||||
SASL: mechanism,
|
||||
}
|
||||
client := &kafka.Client{
|
||||
Addr: kafka.TCP(brokers...),
|
||||
Timeout: timeout,
|
||||
Addr: kafka.TCP(brokers...),
|
||||
// Client.Timeout is a per-request deadline. Keep it unset so the
|
||||
// caller's context (including an explicit queryTimeout) owns it.
|
||||
Timeout: 0,
|
||||
Transport: transport,
|
||||
}
|
||||
return &kafkaGoRuntime{
|
||||
@@ -1060,12 +1062,12 @@ type kafkaParsedSQL struct {
|
||||
}
|
||||
|
||||
var (
|
||||
kafkaSQLFromRE = regexp.MustCompile(`(?i)\bFROM\s+(?:"([^"]+)"|` + "`" + `([^` + "`" + `]+)` + "`" + `|([a-zA-Z0-9_.\-]+))`)
|
||||
kafkaSQLLimitRE = regexp.MustCompile(`(?i)\bLIMIT\s+(\d+)`)
|
||||
kafkaSQLOffsetRE = regexp.MustCompile(`(?i)\bOFFSET\s+(\d+)`)
|
||||
kafkaShowTopicsRE = regexp.MustCompile(`(?i)^\s*SHOW\s+TOPICS(?:\s+LIMIT\s+(\d+))?\s*$`)
|
||||
kafkaDescribeTopicRE = regexp.MustCompile(`(?i)^\s*(?:SHOW|DESCRIBE)\s+TOPIC\s+(?:"([^"]+)"|` + "`" + `([^` + "`" + `]+)` + "`" + `|([a-zA-Z0-9_.\-]+))\s*$`)
|
||||
kafkaConsumeTopicRE = regexp.MustCompile(`(?i)^\s*CONSUME(?:\s+GROUP\s+(?:"([^"]+)"|` + "`" + `([^` + "`" + `]+)` + "`" + `|([a-zA-Z0-9_.\-]+)))?\s+FROM\s+(?:"([^"]+)"|` + "`" + `([^` + "`" + `]+)` + "`" + `|([a-zA-Z0-9_.\-]+))`)
|
||||
kafkaSQLFromRE = regexp.MustCompile(`(?i)\bFROM\s+(?:"([^"]+)"|` + "`" + `([^` + "`" + `]+)` + "`" + `|([a-zA-Z0-9_.\-]+))`)
|
||||
kafkaSQLLimitRE = regexp.MustCompile(`(?i)\bLIMIT\s+(\d+)`)
|
||||
kafkaSQLOffsetRE = regexp.MustCompile(`(?i)\bOFFSET\s+(\d+)`)
|
||||
kafkaShowTopicsRE = regexp.MustCompile(`(?i)^\s*SHOW\s+TOPICS(?:\s+LIMIT\s+(\d+))?\s*$`)
|
||||
kafkaDescribeTopicRE = regexp.MustCompile(`(?i)^\s*(?:SHOW|DESCRIBE)\s+TOPIC\s+(?:"([^"]+)"|` + "`" + `([^` + "`" + `]+)` + "`" + `|([a-zA-Z0-9_.\-]+))\s*$`)
|
||||
kafkaConsumeTopicRE = regexp.MustCompile(`(?i)^\s*CONSUME(?:\s+GROUP\s+(?:"([^"]+)"|` + "`" + `([^` + "`" + `]+)` + "`" + `|([a-zA-Z0-9_.\-]+)))?\s+FROM\s+(?:"([^"]+)"|` + "`" + `([^` + "`" + `]+)` + "`" + `|([a-zA-Z0-9_.\-]+))`)
|
||||
)
|
||||
|
||||
func parseKafkaSQL(sqlText string, defaultLatest bool) (kafkaParsedSQL, bool) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"GoNavi-Wails/internal/connection"
|
||||
|
||||
@@ -12,13 +13,37 @@ import (
|
||||
)
|
||||
|
||||
type fakeKafkaRuntime struct {
|
||||
listTopicsResult []kafkaTopicInfo
|
||||
describeResult kafkaTopicDescription
|
||||
fetchResult []kafkaMessageRecord
|
||||
publishAffected int64
|
||||
lastDescribeTopic string
|
||||
lastFetchRequest kafkaFetchRequest
|
||||
lastPublishCommand kafkaPublishCommand
|
||||
listTopicsResult []kafkaTopicInfo
|
||||
describeResult kafkaTopicDescription
|
||||
fetchResult []kafkaMessageRecord
|
||||
publishAffected int64
|
||||
lastDescribeTopic string
|
||||
lastFetchRequest kafkaFetchRequest
|
||||
lastPublishCommand kafkaPublishCommand
|
||||
}
|
||||
|
||||
func TestKafkaRuntimeDoesNotDeriveRequestTimeoutFromConnectionTimeout(t *testing.T) {
|
||||
runtime, err := newKafkaGoRuntime(connection.ConnectionConfig{
|
||||
Type: "kafka",
|
||||
Host: "127.0.0.1",
|
||||
Port: 9092,
|
||||
Timeout: 1,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("newKafkaGoRuntime: %v", err)
|
||||
}
|
||||
defer runtime.Close()
|
||||
|
||||
concrete, ok := runtime.(*kafkaGoRuntime)
|
||||
if !ok {
|
||||
t.Fatalf("runtime type = %T", runtime)
|
||||
}
|
||||
if concrete.client.Timeout != 0 {
|
||||
t.Fatalf("connection timeout leaked into Kafka request timeout: %s", concrete.client.Timeout)
|
||||
}
|
||||
if concrete.dialer.Timeout != time.Second {
|
||||
t.Fatalf("Kafka dial timeout = %s, want 1s", concrete.dialer.Timeout)
|
||||
}
|
||||
}
|
||||
|
||||
type kafkaOffsetSeekerRecorder struct {
|
||||
|
||||
@@ -515,16 +515,20 @@ func milvusAuthHeaders(config connection.ConnectionConfig) map[string]string {
|
||||
|
||||
func buildMilvusHTTPClient(config connection.ConnectionConfig) *http.Client {
|
||||
transport := http.DefaultTransport.(*http.Transport).Clone()
|
||||
dialTimeout := getConnectTimeout(config)
|
||||
transport.DialContext = (&net.Dialer{Timeout: dialTimeout, KeepAlive: 30 * time.Second}).DialContext
|
||||
if tlsConfig, err := resolveGenericTLSConfig(config); err == nil && tlsConfig != nil {
|
||||
transport.TLSClientConfig = tlsConfig
|
||||
}
|
||||
if config.UseProxy {
|
||||
proxyConfig := config.Proxy
|
||||
transport.DialContext = func(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
return proxytunnel.DialContext(ctx, proxyConfig, network, address)
|
||||
dialCtx, cancel := context.WithTimeout(ctx, dialTimeout)
|
||||
defer cancel()
|
||||
return proxytunnel.DialContext(dialCtx, proxyConfig, network, address)
|
||||
}
|
||||
}
|
||||
return &http.Client{Transport: transport, Timeout: getConnectTimeout(config)}
|
||||
return &http.Client{Transport: transport}
|
||||
}
|
||||
|
||||
func (m *MilvusDB) doJSON(ctx context.Context, method, path string, body interface{}, out interface{}) error {
|
||||
|
||||
@@ -461,16 +461,20 @@ func qdrantAuthHeaders(config connection.ConnectionConfig) map[string]string {
|
||||
|
||||
func buildQdrantHTTPClient(config connection.ConnectionConfig) *http.Client {
|
||||
transport := http.DefaultTransport.(*http.Transport).Clone()
|
||||
dialTimeout := getConnectTimeout(config)
|
||||
transport.DialContext = (&net.Dialer{Timeout: dialTimeout, KeepAlive: 30 * time.Second}).DialContext
|
||||
if tlsConfig, err := resolveGenericTLSConfig(config); err == nil && tlsConfig != nil {
|
||||
transport.TLSClientConfig = tlsConfig
|
||||
}
|
||||
if config.UseProxy {
|
||||
proxyCfg := config.Proxy
|
||||
transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
return proxytunnel.DialContext(ctx, proxyCfg, network, addr)
|
||||
dialCtx, cancel := context.WithTimeout(ctx, dialTimeout)
|
||||
defer cancel()
|
||||
return proxytunnel.DialContext(dialCtx, proxyCfg, network, addr)
|
||||
}
|
||||
}
|
||||
return &http.Client{Transport: transport, Timeout: getConnectTimeout(config)}
|
||||
return &http.Client{Transport: transport}
|
||||
}
|
||||
|
||||
func (q *QdrantDB) doJSON(ctx context.Context, method, path string, body interface{}, out interface{}) error {
|
||||
|
||||
@@ -613,16 +613,20 @@ func buildRabbitMQBaseURL(config connection.ConnectionConfig) string {
|
||||
|
||||
func buildRabbitMQHTTPClient(config connection.ConnectionConfig) *http.Client {
|
||||
transport := http.DefaultTransport.(*http.Transport).Clone()
|
||||
dialTimeout := getConnectTimeout(config)
|
||||
transport.DialContext = (&net.Dialer{Timeout: dialTimeout, KeepAlive: 30 * time.Second}).DialContext
|
||||
if tlsConfig, err := resolveGenericTLSConfig(config); err == nil && tlsConfig != nil {
|
||||
transport.TLSClientConfig = tlsConfig
|
||||
}
|
||||
if config.UseProxy {
|
||||
proxyCfg := config.Proxy
|
||||
transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
return proxytunnel.DialContext(ctx, proxyCfg, network, addr)
|
||||
dialCtx, cancel := context.WithTimeout(ctx, dialTimeout)
|
||||
defer cancel()
|
||||
return proxytunnel.DialContext(dialCtx, proxyCfg, network, addr)
|
||||
}
|
||||
}
|
||||
return &http.Client{Transport: transport, Timeout: getConnectTimeout(config)}
|
||||
return &http.Client{Transport: transport}
|
||||
}
|
||||
|
||||
func rabbitmqAuthHeaders(config connection.ConnectionConfig) map[string]string {
|
||||
|
||||
34
internal/db/timeout_policy_test.go
Normal file
34
internal/db/timeout_policy_test.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"GoNavi-Wails/internal/connection"
|
||||
)
|
||||
|
||||
func TestHTTPDataSourceClientsDoNotUseConnectTimeoutAsRequestTimeout(t *testing.T) {
|
||||
config := connection.ConnectionConfig{Timeout: 1}
|
||||
tests := []struct {
|
||||
name string
|
||||
build func(connection.ConnectionConfig) *http.Client
|
||||
}{
|
||||
{name: "chroma", build: buildChromaHTTPClient},
|
||||
{name: "qdrant", build: buildQdrantHTTPClient},
|
||||
{name: "milvus", build: buildMilvusHTTPClient},
|
||||
{name: "rabbitmq", build: buildRabbitMQHTTPClient},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
client := tt.build(config)
|
||||
if client.Timeout != 0 {
|
||||
t.Fatalf("connection timeout leaked into HTTP request timeout: %s", client.Timeout)
|
||||
}
|
||||
transport, ok := client.Transport.(*http.Transport)
|
||||
if !ok || transport.DialContext == nil {
|
||||
t.Fatal("expected HTTP transport to retain a bounded connection dial")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -261,6 +261,9 @@ func buildTrinoDSN(config connection.ConnectionConfig, customClientName string)
|
||||
}
|
||||
|
||||
params := connectionParamsFromText(config.ConnectionParams)
|
||||
if params == nil {
|
||||
params = url.Values{}
|
||||
}
|
||||
catalog, schema := resolveTrinoNamespace(config.Database, "")
|
||||
if catalog != "" {
|
||||
params.Set("catalog", catalog)
|
||||
@@ -274,9 +277,8 @@ func buildTrinoDSN(config connection.ConnectionConfig, customClientName string)
|
||||
if strings.TrimSpace(params.Get("explicitPrepare")) == "" {
|
||||
params.Set("explicitPrepare", "false")
|
||||
}
|
||||
if strings.TrimSpace(params.Get("query_timeout")) == "" {
|
||||
params.Set("query_timeout", fmt.Sprintf("%ds", getConnectTimeoutSeconds(config)))
|
||||
}
|
||||
// Do not derive Trino's server-side query_timeout from the connection
|
||||
// timeout. The request context is the sole automatic query deadline.
|
||||
if strings.TrimSpace(customClientName) != "" {
|
||||
params.Set("custom_client", strings.TrimSpace(customClientName))
|
||||
}
|
||||
|
||||
@@ -6,8 +6,11 @@ import (
|
||||
"database/sql"
|
||||
"database/sql/driver"
|
||||
"errors"
|
||||
"net/url"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"GoNavi-Wails/internal/connection"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -63,3 +66,24 @@ func TestTrinoCloseCleansStateWhenDatabaseCloseFails(t *testing.T) {
|
||||
t.Fatalf("Close() namespace = %q, want empty", trino.namespace)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildTrinoDSNDoesNotDeriveQueryTimeoutFromConnectionTimeout(t *testing.T) {
|
||||
dsn, err := buildTrinoDSN(connection.ConnectionConfig{
|
||||
Type: "trino",
|
||||
Host: "127.0.0.1",
|
||||
Port: 8080,
|
||||
User: "alice",
|
||||
Database: "hive.analytics",
|
||||
Timeout: 1,
|
||||
}, "")
|
||||
if err != nil {
|
||||
t.Fatalf("buildTrinoDSN: %v", err)
|
||||
}
|
||||
parsed, err := url.Parse(dsn)
|
||||
if err != nil {
|
||||
t.Fatalf("parse Trino DSN: %v", err)
|
||||
}
|
||||
if got := parsed.Query().Get("query_timeout"); got != "" {
|
||||
t.Fatalf("connection timeout leaked into Trino query_timeout=%q", got)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user