diff --git a/CHANGELOG.md b/CHANGELOG.md index 029f621..3108286 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## 开发快照 0.1.358(业务 HTTP 超时不再被探针上限截断) + +### Fixed + +- **F13 超时回归**:`weknoraaccess.Client` 不再把显式配置的 + `TEST_WEKNORA_HTTP_TIMEOUT`(默认 60s)强制截成 10s。只有未配置/非正值才回落到 + 与原 appconfig 一致的 60s;readiness 探测自身的 10s 上限保持独立。11s 本地响应在 + 60s 配置下可完成,显式 50ms 短超时仍生效。 + ## 开发快照 0.1.357(取件后自动解析、定位一次性请求、worker 有效配置) ### Fixed diff --git a/internal/weknoraaccess/client.go b/internal/weknoraaccess/client.go index d7524d4..2ce9dab 100644 --- a/internal/weknoraaccess/client.go +++ b/internal/weknoraaccess/client.go @@ -11,6 +11,10 @@ import ( var ErrNotConfigured = errors.New("WeKnora 有效端点/凭据未配置") +// defaultBusinessTimeout matches appconfig's TEST_WEKNORA_HTTP_TIMEOUT default. +// The readiness probe has its own 10s cap; it must not bound upload/poll calls. +const defaultBusinessTimeout = 60 * time.Second + // Client resolves the effective WeKnora target on every call. It deliberately // owns no cached *weknora.Client: endpoint, key and binding are console-managed. type Client struct { @@ -28,8 +32,8 @@ func (client *Client) with(ctx context.Context, call func(*weknora.Client) error return ErrNotConfigured } timeout := client.Timeout - if timeout <= 0 || timeout > 10*time.Second { - timeout = 10 * time.Second + if timeout <= 0 { + timeout = defaultBusinessTimeout } remote, err := weknora.NewClient(weknora.ClientConfig{ BaseURL: target.BaseURL, diff --git a/internal/weknoraaccess/client_test.go b/internal/weknoraaccess/client_test.go index 54be5bf..6faedc2 100644 --- a/internal/weknoraaccess/client_test.go +++ b/internal/weknoraaccess/client_test.go @@ -51,3 +51,60 @@ func TestDynamicClientReResolvesEndpointKeyAndBinding(t *testing.T) { t.Fatalf("rotated endpoint/key = %#v, %v", knowledge, err) } } + +// F13 timeout regression: the configured business timeout (default 60s) must +// reach upload/poll clients unchanged; only a missing/zero value falls back. +// The readiness probe's 10s cap must not leak into business HTTP. +func TestConfiguredBusinessTimeoutIsNotCappedToProbeTimeout(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + select { + case <-time.After(11 * time.Second): + _, _ = w.Write([]byte(`{"success":true,"data":{"id":"slow-but-valid"}}`)) + case <-r.Context().Done(): + } + })) + defer server.Close() + client := &Client{ + Resolver: readiness.Resolver{ + Endpoint: func(context.Context) string { return server.URL }, + ReadKey: func() (string, error) { return "isolated-test-key", nil }, + DefaultKB: "isolated-test-kb", + }, + Timeout: 60 * time.Second, + } + started := time.Now() + item, err := client.GetKnowledge(context.Background(), "slow-but-valid") + if err != nil { + t.Fatalf("60s business timeout rejected 11s response after %s: %v", time.Since(started), err) + } + if item.ID != "slow-but-valid" { + t.Fatalf("wrong response identity: %q", item.ID) + } +} + +// An explicit short timeout is still honoured for callers that need it. +func TestExplicitShortBusinessTimeoutStillApplies(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + select { + case <-time.After(300 * time.Millisecond): + _, _ = w.Write([]byte(`{"success":true,"data":{"id":"too-late"}}`)) + case <-r.Context().Done(): + } + })) + defer server.Close() + client := &Client{ + Resolver: readiness.Resolver{ + Endpoint: func(context.Context) string { return server.URL }, + ReadKey: func() (string, error) { return "isolated-test-key", nil }, + DefaultKB: "isolated-test-kb", + }, + Timeout: 50 * time.Millisecond, + } + started := time.Now() + if _, err := client.GetKnowledge(context.Background(), "too-late"); err == nil { + t.Fatal("explicit 50ms timeout did not bound the call") + } + if elapsed := time.Since(started); elapsed > 2*time.Second { + t.Fatalf("explicit short timeout took %s", elapsed) + } +}