From fcec494ea8378b9d82c4d7203c664e4abf595658 Mon Sep 17 00:00:00 2001 From: Kevin Wan Date: Thu, 25 Dec 2025 21:39:45 +0800 Subject: [PATCH] fix: ignore context cancel on triggering breaker of httpc (#5360) Signed-off-by: kevin --- .github/copilot-instructions.md | 105 ++++++++++++- go.mod | 29 ++-- go.sum | 58 +++---- rest/httpc/service.go | 41 ++++- rest/httpc/service_test.go | 257 ++++++++++++++++++++++++++++++++ 5 files changed, 446 insertions(+), 44 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 9c89a6b67..bf475b2f8 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -172,6 +172,109 @@ err := c.QueryRowCtx(ctx, &dest, key, func(ctx context.Context, conn sqlx.SqlCon 3. **API documentation**: Maintain API documentation in sync 4. **README updates**: Update README for significant changes +## GitHub Issue Management + +### Understanding and Categorizing Issues + +When analyzing GitHub issues, consider these common categories: + +1. **Bug Reports**: Stack traces, version info, reproduction steps +2. **Feature Requests**: Use case, proposed solution, alternatives +3. **Questions**: Usage, configuration, or architecture +4. **Documentation Issues**: Missing, unclear, or incorrect docs +5. **Performance Issues**: Benchmarks, profiling data, resource usage + +### Issue Analysis Checklist + +- Identify affected component (REST, RPC, Gateway, MCP, Core utilities, goctl) +- Check versions (go-zero, Go) +- Look for reproduction steps or code examples +- Review code snippets, logs, or stack traces +- Check if related to resilience features (breaker, load shedding, rate limiting) +- Determine production impact + +### Responding to Issues + +Be helpful and professional. Ask clarifying questions when needed. Reference relevant documentation and code files. Provide code examples following project conventions. Suggest workarounds when applicable. + +### Chinese to English Translation + +go-zero has an international user base. When encountering issues or comments written in Chinese, translate them to English to ensure all contributors can participate in discussions. + +#### Translation Guidelines + +1. **Update issue titles**: Edit the issue title to include English translation only +2. **Translate comments in place**: Add a comment with the English translation, followed by the original Chinese text +3. **Keep original Chinese**: After translating, include the original Chinese text in a blockquote for verification +4. **Encourage English communication**: Politely suggest users write in English for better collaboration +5. **Maintain technical accuracy**: Preserve technical terms, component names, and code exactly +6. **Translate naturally**: Avoid literal word-by-word translation; use idiomatic English +7. **Preserve formatting**: Keep markdown formatting, code blocks, and links intact +8. **Keep URLs unchanged**: Don't translate URLs or file paths + +#### Common Technical Terms (Chinese → English) + +- 框架 → **Framework** | 中间件 → **Middleware** | 负载均衡 → **Load Balancing** +- 熔断器 → **Circuit Breaker** | 限流 → **Rate Limiting** | 降载/过载保护 → **Load Shedding** +- 服务发现 → **Service Discovery** | 配置 → **Configuration** | 弹性/容错 → **Resilience** | 微服务 → **Microservices** + +#### Translation Example + +**Original Chinese Title:** `goctl 执行环境问题` +**Updated Title:** `goctl Execution Environment Issue` + +**Original Chinese Comment:** `我在项目中遇到熔断器配置问题` +**Translation in Comment:** +```markdown +I encountered a circuit breaker configuration issue in my project. + +> Original (原文): 我在项目中遇到熔断器配置问题 +``` + +### Common Issue Patterns and Solutions + +#### Configuration Issues +- Check `service.ServiceConf` embedding and struct tags +- Verify YAML syntax, defaults, and validation rules +- Reference: [rest/config.go](rest/config.go), [zrpc/config.go](zrpc/config.go) + +#### Code Generation (goctl) Issues +- Verify `.api` or `.proto` file syntax and goctl version +- Reference: `tools/goctl/` directory + +#### RPC Connection Issues +- Check etcd configuration, service discovery, and endpoints +- Verify load balancing settings (p2c_ewma) + +#### Database/Cache Issues +- Verify `sqlx.SqlConn` usage with context +- Check cache key generation, invalidation, and connection pools +- Use test helpers (`redistest`, `mongtest`) + +#### Performance Issues +- Check if load shedding is enabled (mode: `pre`/`pro`) +- Review circuit breaker thresholds, rate limiting, and context timeouts + +### Referencing Codebase + +When explaining issues, reference specific files and patterns: +- REST API: `rest/`, `rest/handler/`, `rest/httpx/` +- RPC: `zrpc/`, `zrpc/internal/` +- Core utilities: `core/breaker/`, `core/limit/`, `core/load/`, etc. +- Gateway: `gateway/` +- MCP: `mcp/` +- Code generation: `tools/goctl/` +- Examples: `adhoc/` directory contains various examples + +### Encouraging Best Practices + +When responding to issues, gently guide users toward: +- Proper error handling with context +- Using resilience features (breakers, rate limiters) +- Following testing patterns with table-driven tests +- Implementing proper resource cleanup +- Reading existing documentation in `docs/` and `readme.md` + ## Common Patterns to Follow ### Service Configuration @@ -203,7 +306,7 @@ Always implement proper resource cleanup using defer and context cancellation. ## Build and Test Commands - Build: `go build ./...` -- Test: `go test ./...` +- Test: `go test ./...` - Test with race detection: `go test -race ./...` - Format: `gofmt -w .` - Code generation: diff --git a/go.mod b/go.mod index 9fea492de..975cef04e 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,7 @@ require ( github.com/jackc/pgx/v5 v5.7.4 github.com/jhump/protoreflect v1.17.0 github.com/pelletier/go-toml/v2 v2.2.4 - github.com/prometheus/client_golang v1.21.1 + github.com/prometheus/client_golang v1.23.2 github.com/redis/go-redis/v9 v9.17.2 github.com/spaolacci/murmur3 v1.1.0 github.com/stretchr/testify v1.11.1 @@ -33,19 +33,19 @@ require ( go.uber.org/automaxprocs v1.6.0 go.uber.org/goleak v1.3.0 go.uber.org/mock v0.6.0 - golang.org/x/net v0.35.0 - golang.org/x/sys v0.30.0 + golang.org/x/net v0.43.0 + golang.org/x/sys v0.35.0 golang.org/x/time v0.10.0 google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d google.golang.org/grpc v1.65.0 - google.golang.org/protobuf v1.36.5 + google.golang.org/protobuf v1.36.11 gopkg.in/cheggaaa/pb.v1 v1.0.28 gopkg.in/h2non/gock.v1 v1.1.2 gopkg.in/yaml.v2 v2.4.0 k8s.io/api v0.29.3 k8s.io/apimachinery v0.29.4 k8s.io/client-go v0.29.3 - k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 + k8s.io/utils v0.0.0-20251222233032-718f0e51e6d2 ) require ( @@ -70,7 +70,7 @@ require ( github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/snappy v1.0.0 // indirect github.com/google/gnostic-models v0.6.8 // indirect - github.com/google/go-cmp v0.6.0 // indirect + github.com/google/go-cmp v0.7.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/grafana/pyroscope-go/godeltaprof v0.1.9 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect @@ -80,7 +80,7 @@ require ( github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/compress v1.17.11 // indirect + github.com/klauspost/compress v1.18.0 // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect @@ -91,9 +91,9 @@ require ( github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/openzipkin/zipkin-go v0.4.3 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/prometheus/client_model v0.6.1 // indirect - github.com/prometheus/common v0.62.0 // indirect - github.com/prometheus/procfs v0.15.1 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.66.1 // indirect + github.com/prometheus/procfs v0.16.1 // indirect github.com/rivo/uniseg v0.2.0 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect @@ -108,11 +108,12 @@ require ( go.uber.org/atomic v1.10.0 // indirect go.uber.org/multierr v1.9.0 // indirect go.uber.org/zap v1.24.0 // indirect - golang.org/x/crypto v0.33.0 // indirect - golang.org/x/oauth2 v0.24.0 // indirect + go.yaml.in/yaml/v2 v2.4.2 // indirect + golang.org/x/crypto v0.41.0 // indirect + golang.org/x/oauth2 v0.30.0 // indirect golang.org/x/sync v0.16.0 // indirect - golang.org/x/term v0.29.0 // indirect - golang.org/x/text v0.22.0 // indirect + golang.org/x/term v0.34.0 // indirect + golang.org/x/text v0.28.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index fa51457a0..3256fb89f 100644 --- a/go.sum +++ b/go.sum @@ -69,8 +69,8 @@ github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEW github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -103,8 +103,8 @@ github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHm github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE= -github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= -github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= @@ -146,14 +146,14 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= -github.com/prometheus/client_golang v1.21.1 h1:DOvXXTqVzvkIewV/CDPFdejpMCGeMcbGCQ8YOmu+Ibk= -github.com/prometheus/client_golang v1.21.1/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg= -github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= -github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= -github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= -github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= -github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= -github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= +github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= +github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= +github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= github.com/redis/go-redis/v9 v9.17.2 h1:P2EGsA4qVIM3Pp+aPocCJ7DguDHhqrXNhVcEp4ViluI= github.com/redis/go-redis/v9 v9.17.2/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= @@ -231,12 +231,14 @@ go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= +go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= +go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= -golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= +golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= +golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= @@ -246,10 +248,10 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= -golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= -golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE= -golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= +golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= +golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= +golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -265,18 +267,18 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= -golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= +golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= -golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= +golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4= +golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= -golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= +golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= +golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4= golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -296,8 +298,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 h1: google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc= google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= -google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= -google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= @@ -323,8 +325,8 @@ k8s.io/klog/v2 v2.110.1 h1:U/Af64HJf7FcwMcXyKm2RPM22WZzyR7OSpYj5tg3cL0= k8s.io/klog/v2 v2.110.1/go.mod h1:YGtd1984u+GgbuZ7e08/yBuAfKLSO0+uR1Fhi6ExXjo= k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 h1:aVUu9fTY98ivBPKR9Y5w/AuzbMm96cd3YHRTU83I780= k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00/go.mod h1:AsvuZPBlUDVuCdzJ87iajxtXuR9oktsTctW/R9wwouA= -k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1J8+AsQnQCKsi8A= -k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +k8s.io/utils v0.0.0-20251222233032-718f0e51e6d2 h1:OfgiEo21hGiwx1oJUU5MpEaeOEg6coWndBkZF/lkFuE= +k8s.io/utils v0.0.0-20251222233032-718f0e51e6d2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= diff --git a/rest/httpc/service.go b/rest/httpc/service.go index a65880e32..1524dee6d 100644 --- a/rest/httpc/service.go +++ b/rest/httpc/service.go @@ -2,7 +2,10 @@ package httpc import ( "context" + "errors" + "net" "net/http" + "net/url" "github.com/zeromicro/go-zero/core/breaker" ) @@ -67,8 +70,44 @@ func (s namedService) do(r *http.Request) (resp *http.Response, err error) { resp, err = s.cli.Do(r) return err }, func(err error) bool { - return err == nil && resp.StatusCode < http.StatusInternalServerError + return acceptable(resp, err) }) return } + +// acceptable determines whether the HTTP request/response should be considered +// successful for circuit breaker purposes. +// +// Returns true (acceptable) for: +// - HTTP status codes < 500 (2xx, 3xx, 4xx) +// - Context cancellation (user-initiated) +// - Non-network errors (application-level errors) +// +// Returns false (not acceptable, triggers breaker) for: +// - HTTP status codes >= 500 (server errors) +// - context.DeadlineExceeded (timeout) +// - Network errors (connection refused, DNS failures, etc.) +func acceptable(resp *http.Response, err error) bool { + if err == nil { + return resp.StatusCode < http.StatusInternalServerError + } + + if errors.Is(err, context.DeadlineExceeded) { + return false + } + + if errors.Is(err, context.Canceled) { + return true + } + + // Unwrap url.Error if present + var ue *url.Error + if errors.As(err, &ue) { + err = ue.Unwrap() + } + + // Network errors are not acceptable + var ne net.Error + return !errors.As(err, &ne) +} diff --git a/rest/httpc/service_test.go b/rest/httpc/service_test.go index 8e49ddbc9..a5b41b721 100644 --- a/rest/httpc/service_test.go +++ b/rest/httpc/service_test.go @@ -2,9 +2,13 @@ package httpc import ( "context" + "errors" + "net" "net/http" "net/http/httptest" + "net/url" "testing" + "time" "github.com/stretchr/testify/assert" "github.com/zeromicro/go-zero/rest/internal/header" @@ -84,3 +88,256 @@ func TestNamedService_DoBadRequest(t *testing.T) { _, err := service.Do(context.Background(), http.MethodPost, "/nodes/:key", val) assert.NotNil(t, err) } + +// mockNetError implements net.Error interface for testing +type mockNetError struct { + msg string + timeout bool + temporary bool +} + +func (e *mockNetError) Error() string { return e.msg } +func (e *mockNetError) Timeout() bool { return e.timeout } +func (e *mockNetError) Temporary() bool { return e.temporary } + +func TestAcceptable(t *testing.T) { + tests := []struct { + name string + resp *http.Response + err error + expected bool + }{ + { + name: "no error with 2xx status code", + resp: &http.Response{ + StatusCode: http.StatusOK, + }, + err: nil, + expected: true, + }, + { + name: "no error with 3xx status code", + resp: &http.Response{ + StatusCode: http.StatusMovedPermanently, + }, + err: nil, + expected: true, + }, + { + name: "no error with 4xx status code", + resp: &http.Response{ + StatusCode: http.StatusNotFound, + }, + err: nil, + expected: true, + }, + { + name: "no error with 499 status code (just below 500)", + resp: &http.Response{ + StatusCode: 499, + }, + err: nil, + expected: true, + }, + { + name: "no error with 500 status code", + resp: &http.Response{ + StatusCode: http.StatusInternalServerError, + }, + err: nil, + expected: false, + }, + { + name: "no error with 503 status code", + resp: &http.Response{ + StatusCode: http.StatusServiceUnavailable, + }, + err: nil, + expected: false, + }, + { + name: "context deadline exceeded", + resp: nil, + err: context.DeadlineExceeded, + expected: false, + }, + { + name: "context canceled", + resp: nil, + err: context.Canceled, + expected: true, + }, + { + name: "wrapped context deadline exceeded", + resp: nil, + err: errors.Join(context.DeadlineExceeded, errors.New("timeout")), + expected: false, + }, + { + name: "wrapped context canceled", + resp: nil, + err: errors.Join(context.Canceled, errors.New("canceled")), + expected: true, + }, + { + name: "network error - timeout", + resp: nil, + err: &mockNetError{msg: "network timeout", timeout: true, temporary: false}, + expected: false, + }, + { + name: "network error - temporary", + resp: nil, + err: &mockNetError{msg: "temporary network error", timeout: false, temporary: true}, + expected: false, + }, + { + name: "network error - connection refused", + resp: nil, + err: &net.OpError{Op: "dial", Net: "tcp", Err: errors.New("connection refused")}, + expected: false, + }, + { + name: "url.Error wrapping network error", + resp: nil, + err: &url.Error{ + Op: "Get", + URL: "http://example.com", + Err: &mockNetError{msg: "network error", timeout: true}, + }, + expected: false, + }, + { + name: "url.Error wrapping non-network error", + resp: nil, + err: &url.Error{ + Op: "Get", + URL: "http://example.com", + Err: errors.New("some other error"), + }, + expected: true, + }, + { + name: "url.Error wrapping context.DeadlineExceeded", + resp: nil, + err: &url.Error{ + Op: "Get", + URL: "http://example.com", + Err: context.DeadlineExceeded, + }, + expected: false, + }, + { + name: "url.Error wrapping context.Canceled", + resp: nil, + err: &url.Error{ + Op: "Get", + URL: "http://example.com", + Err: context.Canceled, + }, + expected: true, + }, + { + name: "generic error (non-network)", + resp: nil, + err: errors.New("some random error"), + expected: true, + }, + { + name: "EOF error (non-network)", + resp: nil, + err: errors.New("EOF"), + expected: true, + }, + { + name: "nil response with nil error (edge case)", + resp: nil, + err: nil, + expected: false, // Will panic in real code, but resp.StatusCode access + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Handle the edge case where resp is nil and err is nil + if tt.resp == nil && tt.err == nil { + // This would panic in real code, so we skip the actual test + // In production, this should never happen + return + } + + result := acceptable(tt.resp, tt.err) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestAcceptable_RealNetworkTimeout(t *testing.T) { + // Create a client with very short timeout + client := &http.Client{ + Timeout: 1 * time.Nanosecond, // Extremely short timeout to force timeout error + } + + service := NewServiceWithClient("test", client) + + // Create a server that delays response + svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + time.Sleep(100 * time.Millisecond) + w.WriteHeader(http.StatusOK) + })) + defer svr.Close() + + req, err := http.NewRequest(http.MethodGet, svr.URL, nil) + assert.NoError(t, err) + + // This should timeout and trigger the circuit breaker + resp, err := service.DoRequest(req) + + // The error should be present due to timeout + assert.Error(t, err) + // Response might be nil due to timeout + if resp != nil { + t.Logf("Response status: %d", resp.StatusCode) + } +} + +func TestAcceptable_Integration(t *testing.T) { + tests := []struct { + name string + statusCode int + expectBreaker bool // Whether breaker should consider this as failure + }{ + {"200 OK should not trigger breaker", http.StatusOK, false}, + {"201 Created should not trigger breaker", http.StatusCreated, false}, + {"400 Bad Request should not trigger breaker", http.StatusBadRequest, false}, + {"404 Not Found should not trigger breaker", http.StatusNotFound, false}, + {"500 Internal Server Error should trigger breaker", http.StatusInternalServerError, true}, + {"502 Bad Gateway should trigger breaker", http.StatusBadGateway, true}, + {"503 Service Unavailable should trigger breaker", http.StatusServiceUnavailable, true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(tt.statusCode) + })) + defer svr.Close() + + service := NewService("test-service-" + tt.name) + req, err := http.NewRequest(http.MethodGet, svr.URL, nil) + assert.NoError(t, err) + + resp, err := service.DoRequest(req) + assert.NoError(t, err) + assert.Equal(t, tt.statusCode, resp.StatusCode) + + // The actual breaker behavior is tested implicitly through the acceptable function + result := acceptable(resp, nil) + if tt.expectBreaker { + assert.False(t, result, "Status %d should not be acceptable", tt.statusCode) + } else { + assert.True(t, result, "Status %d should be acceptable", tt.statusCode) + } + }) + } +}