mirror of
https://github.com/gin-gonic/gin.git
synced 2026-06-04 17:58:14 +08:00
claude code不添加skill和插件生成的代码
This commit is contained in:
parent
d3ffc99852
commit
47de2f52db
45
config.go
Normal file
45
config.go
Normal file
@ -0,0 +1,45 @@
|
||||
// Copyright 2025 Gin Core Team. All rights reserved.
|
||||
// Use of this source code is governed by a MIT style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package gin
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/goccy/go-yaml"
|
||||
)
|
||||
|
||||
// Config represents the YAML configuration structure for Gin.
|
||||
type Config struct {
|
||||
// MaxConns limits the maximum number of concurrent connections.
|
||||
// 0 means no limit (default behavior).
|
||||
MaxConns int64 `yaml:"max_conns"`
|
||||
}
|
||||
|
||||
// LoadConfig reads configuration from a YAML file and returns an OptionFunc
|
||||
// that applies the configuration to an Engine.
|
||||
func LoadConfig(path string) (OptionFunc, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var cfg Config
|
||||
if err := yaml.Unmarshal(data, &cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return func(e *Engine) {
|
||||
if cfg.MaxConns > 0 {
|
||||
e.MaxConns = cfg.MaxConns
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
// WithMaxConns creates an OptionFunc that sets the maximum concurrent connections.
|
||||
func WithMaxConns(n int64) OptionFunc {
|
||||
return func(e *Engine) {
|
||||
e.MaxConns = n
|
||||
}
|
||||
}
|
||||
72
config_test.go
Normal file
72
config_test.go
Normal file
@ -0,0 +1,72 @@
|
||||
// Copyright 2025 Gin Core Team. All rights reserved.
|
||||
// Use of this source code is governed by a MIT style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package gin
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestLoadConfig(t *testing.T) {
|
||||
path := filepath.Join("testdata", "config.yaml")
|
||||
opt, err := LoadConfig(path)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, opt)
|
||||
|
||||
engine := New()
|
||||
opt(engine)
|
||||
assert.Equal(t, int64(100), engine.MaxConns)
|
||||
}
|
||||
|
||||
func TestLoadConfigNotFound(t *testing.T) {
|
||||
opt, err := LoadConfig("nonexistent.yaml")
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, opt)
|
||||
}
|
||||
|
||||
func TestLoadConfigInvalidYAML(t *testing.T) {
|
||||
// Create a temporary file with invalid YAML
|
||||
tmpFile, err := os.CreateTemp("", "invalid_*.yaml")
|
||||
require.NoError(t, err)
|
||||
defer os.Remove(tmpFile.Name())
|
||||
|
||||
_, err = tmpFile.WriteString("max_conns: [invalid")
|
||||
require.NoError(t, err)
|
||||
tmpFile.Close()
|
||||
|
||||
opt, err := LoadConfig(tmpFile.Name())
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, opt)
|
||||
}
|
||||
|
||||
func TestLoadConfigEmpty(t *testing.T) {
|
||||
// Create a temporary empty config file
|
||||
tmpFile, err := os.CreateTemp("", "empty_*.yaml")
|
||||
require.NoError(t, err)
|
||||
defer os.Remove(tmpFile.Name())
|
||||
tmpFile.Close()
|
||||
|
||||
opt, err := LoadConfig(tmpFile.Name())
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, opt)
|
||||
|
||||
engine := New()
|
||||
opt(engine)
|
||||
assert.Equal(t, int64(0), engine.MaxConns)
|
||||
}
|
||||
|
||||
func TestWithMaxConns(t *testing.T) {
|
||||
engine := New(WithMaxConns(50))
|
||||
assert.Equal(t, int64(50), engine.MaxConns)
|
||||
}
|
||||
|
||||
func TestWithMaxConnsZero(t *testing.T) {
|
||||
engine := New(WithMaxConns(0))
|
||||
assert.Equal(t, int64(0), engine.MaxConns)
|
||||
}
|
||||
32
gin.go
32
gin.go
@ -172,6 +172,10 @@ type Engine struct {
|
||||
// ContextWithFallback enable fallback Context.Deadline(), Context.Done(), Context.Err() and Context.Value() when Context.Request.Context() is not nil.
|
||||
ContextWithFallback bool
|
||||
|
||||
// MaxConns limits the maximum number of concurrent connections.
|
||||
// 0 means no limit (default behavior).
|
||||
MaxConns int64
|
||||
|
||||
delims render.Delims
|
||||
secureJSONPrefix string
|
||||
HTMLRender render.HTMLRender
|
||||
@ -534,6 +538,17 @@ func parseIP(ip string) net.IP {
|
||||
return parsedIP
|
||||
}
|
||||
|
||||
// wrapListener wraps a net.Listener with connection limiting if MaxConns > 0.
|
||||
func (engine *Engine) wrapListener(ln net.Listener) net.Listener {
|
||||
if engine.MaxConns <= 0 {
|
||||
return ln
|
||||
}
|
||||
return &limitedListener{
|
||||
Listener: ln,
|
||||
sem: make(chan struct{}, engine.MaxConns),
|
||||
}
|
||||
}
|
||||
|
||||
// Run attaches the router to a http.Server and starts listening and serving HTTP requests.
|
||||
// It is a shortcut for http.ListenAndServe(addr, router)
|
||||
// Note: this method will block the calling goroutine indefinitely unless an error happens.
|
||||
@ -547,11 +562,15 @@ func (engine *Engine) Run(addr ...string) (err error) {
|
||||
engine.updateRouteTrees()
|
||||
address := resolveAddress(addr)
|
||||
debugPrint("Listening and serving HTTP on %s\n", address)
|
||||
ln, err := net.Listen("tcp", address)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
server := &http.Server{ // #nosec G112
|
||||
Addr: address,
|
||||
Handler: engine.Handler(),
|
||||
}
|
||||
err = server.ListenAndServe()
|
||||
err = server.Serve(engine.wrapListener(ln))
|
||||
return
|
||||
}
|
||||
|
||||
@ -571,7 +590,11 @@ func (engine *Engine) RunTLS(addr, certFile, keyFile string) (err error) {
|
||||
Addr: addr,
|
||||
Handler: engine.Handler(),
|
||||
}
|
||||
err = server.ListenAndServeTLS(certFile, keyFile)
|
||||
ln, err := net.Listen("tcp", addr)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = server.ServeTLS(engine.wrapListener(ln), certFile, keyFile)
|
||||
return
|
||||
}
|
||||
|
||||
@ -597,7 +620,7 @@ func (engine *Engine) RunUnix(file string) (err error) {
|
||||
server := &http.Server{ // #nosec G112
|
||||
Handler: engine.Handler(),
|
||||
}
|
||||
err = server.Serve(listener)
|
||||
err = server.Serve(engine.wrapListener(listener))
|
||||
return
|
||||
}
|
||||
|
||||
@ -627,6 +650,7 @@ func (engine *Engine) RunFd(fd int) (err error) {
|
||||
// RunQUIC attaches the router to a http.Server and starts listening and serving QUIC requests.
|
||||
// It is a shortcut for http3.ListenAndServeQUIC(addr, certFile, keyFile, router)
|
||||
// Note: this method will block the calling goroutine indefinitely unless an error happens.
|
||||
// Note: MaxConns is not supported for QUIC due to protocol limitations.
|
||||
func (engine *Engine) RunQUIC(addr, certFile, keyFile string) (err error) {
|
||||
debugPrint("Listening and serving QUIC on %s\n", addr)
|
||||
defer func() { debugPrintError(err) }()
|
||||
@ -654,7 +678,7 @@ func (engine *Engine) RunListener(listener net.Listener) (err error) {
|
||||
server := &http.Server{ // #nosec G112
|
||||
Handler: engine.Handler(),
|
||||
}
|
||||
err = server.Serve(listener)
|
||||
err = server.Serve(engine.wrapListener(listener))
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
47
listener.go
Normal file
47
listener.go
Normal file
@ -0,0 +1,47 @@
|
||||
// Copyright 2025 Gin Core Team. All rights reserved.
|
||||
// Use of this source code is governed by a MIT style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package gin
|
||||
|
||||
import (
|
||||
"net"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// limitedListener wraps a net.Listener and limits the number of concurrent connections
|
||||
// using a buffered channel as a semaphore.
|
||||
type limitedListener struct {
|
||||
net.Listener
|
||||
sem chan struct{}
|
||||
}
|
||||
|
||||
// Accept accepts a new connection. If the connection limit has been reached,
|
||||
// the new connection is immediately closed.
|
||||
func (l *limitedListener) Accept() (net.Conn, error) {
|
||||
conn, err := l.Listener.Accept()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
select {
|
||||
case l.sem <- struct{}{}:
|
||||
return &limitedConn{Conn: conn, sem: l.sem}, nil
|
||||
default:
|
||||
conn.Close()
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
// limitedConn wraps a net.Conn and releases the semaphore slot on Close.
|
||||
type limitedConn struct {
|
||||
net.Conn
|
||||
sem chan struct{}
|
||||
closeOnce sync.Once
|
||||
}
|
||||
|
||||
// Close closes the connection and releases the semaphore slot.
|
||||
func (c *limitedConn) Close() error {
|
||||
c.closeOnce.Do(func() { <-c.sem })
|
||||
return c.Conn.Close()
|
||||
}
|
||||
163
listener_test.go
Normal file
163
listener_test.go
Normal file
@ -0,0 +1,163 @@
|
||||
// Copyright 2025 Gin Core Team. All rights reserved.
|
||||
// Use of this source code is governed by a MIT style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package gin
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestLimitedListenerUnderLimit(t *testing.T) {
|
||||
router := New(WithMaxConns(10))
|
||||
router.GET("/", func(c *Context) {
|
||||
c.String(http.StatusOK, "ok")
|
||||
})
|
||||
|
||||
server := httptest.NewServer(router.Handler())
|
||||
defer server.Close()
|
||||
|
||||
// Should be able to make requests under limit
|
||||
resp, err := http.Get(server.URL)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
resp.Body.Close()
|
||||
}
|
||||
|
||||
func TestLimitedListenerAtLimit(t *testing.T) {
|
||||
// Create a server that holds connections
|
||||
var activeConns atomic.Int32
|
||||
var wg sync.WaitGroup
|
||||
block := make(chan struct{})
|
||||
|
||||
router := New(WithMaxConns(2))
|
||||
router.GET("/", func(c *Context) {
|
||||
activeConns.Add(1)
|
||||
wg.Add(1)
|
||||
<-block // Block until test releases
|
||||
activeConns.Add(-1)
|
||||
wg.Done()
|
||||
c.String(http.StatusOK, "ok")
|
||||
})
|
||||
|
||||
server := httptest.NewServer(router.Handler())
|
||||
defer server.Close()
|
||||
|
||||
// Start 2 requests that will block
|
||||
for i := 0; i < 2; i++ {
|
||||
go func() {
|
||||
resp, err := http.Get(server.URL)
|
||||
if err == nil {
|
||||
resp.Body.Close()
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Wait for both connections to be active
|
||||
require.Eventually(t, func() bool {
|
||||
return activeConns.Load() == 2
|
||||
}, 2*time.Second, 10*time.Millisecond)
|
||||
|
||||
// Third request should be rejected immediately
|
||||
client := &http.Client{Timeout: 500 * time.Millisecond}
|
||||
_, err := client.Get(server.URL)
|
||||
// Connection should be rejected
|
||||
assert.Error(t, err, "expected connection to be rejected")
|
||||
|
||||
// Release the blocked connections
|
||||
close(block)
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func TestLimitedConnRelease(t *testing.T) {
|
||||
block := make(chan struct{})
|
||||
|
||||
router := New(WithMaxConns(1))
|
||||
router.GET("/", func(c *Context) {
|
||||
<-block
|
||||
c.String(http.StatusOK, "ok")
|
||||
})
|
||||
|
||||
server := httptest.NewServer(router.Handler())
|
||||
defer server.Close()
|
||||
|
||||
// Start one blocking request
|
||||
var firstDone atomic.Bool
|
||||
go func() {
|
||||
resp, err := http.Get(server.URL)
|
||||
if err == nil {
|
||||
resp.Body.Close()
|
||||
firstDone.Store(true)
|
||||
}
|
||||
}()
|
||||
|
||||
// Give the first request time to start
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
// Second request should fail
|
||||
client := &http.Client{Timeout: 200 * time.Millisecond}
|
||||
_, err := client.Get(server.URL)
|
||||
assert.Error(t, err, "expected connection to be rejected when limit reached")
|
||||
|
||||
// Release the first connection
|
||||
close(block)
|
||||
|
||||
// Eventually first request should complete
|
||||
require.Eventually(t, firstDone.Load, 2*time.Second, 10*time.Millisecond)
|
||||
}
|
||||
|
||||
func TestLimitedListenerZeroLimit(t *testing.T) {
|
||||
router := New(WithMaxConns(0))
|
||||
router.GET("/", func(c *Context) {
|
||||
c.String(http.StatusOK, "ok")
|
||||
})
|
||||
|
||||
server := httptest.NewServer(router.Handler())
|
||||
defer server.Close()
|
||||
|
||||
// Should allow unlimited connections (zero means no limit)
|
||||
for i := 0; i < 5; i++ {
|
||||
resp, err := http.Get(server.URL)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
resp.Body.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func TestWrapListenerNoLimit(t *testing.T) {
|
||||
engine := New()
|
||||
assert.Equal(t, int64(0), engine.MaxConns)
|
||||
|
||||
// Create a dummy listener
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
require.NoError(t, err)
|
||||
defer ln.Close()
|
||||
|
||||
// Should return original listener when no limit
|
||||
wrapped := engine.wrapListener(ln)
|
||||
assert.Equal(t, ln, wrapped)
|
||||
}
|
||||
|
||||
func TestWrapListenerWithLimit(t *testing.T) {
|
||||
engine := New(WithMaxConns(5))
|
||||
|
||||
// Create a dummy listener
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
require.NoError(t, err)
|
||||
defer ln.Close()
|
||||
|
||||
// Should return wrapped listener
|
||||
wrapped := engine.wrapListener(ln)
|
||||
assert.NotNil(t, wrapped)
|
||||
_, ok := wrapped.(*limitedListener)
|
||||
assert.True(t, ok)
|
||||
}
|
||||
637
my_changes.patch
Normal file
637
my_changes.patch
Normal file
@ -0,0 +1,637 @@
|
||||
diff --git a/claude.change.patch b/claude.change.patch
|
||||
new file mode 100644
|
||||
index 0000000..be3867b
|
||||
--- /dev/null
|
||||
+++ b/claude.change.patch
|
||||
@@ -0,0 +1,89 @@
|
||||
+diff --git a/gin.go b/gin.go
|
||||
+index 2e033bf..2ccc093 100644
|
||||
+--- a/gin.go
|
||||
++++ b/gin.go
|
||||
+@@ -172,6 +172,10 @@ type Engine struct {
|
||||
+ // ContextWithFallback enable fallback Context.Deadline(), Context.Done(), Context.Err() and Context.Value() when Context.Request.Context() is not nil.
|
||||
+ ContextWithFallback bool
|
||||
+
|
||||
++ // MaxConns limits the maximum number of concurrent connections.
|
||||
++ // 0 means no limit (default behavior).
|
||||
++ MaxConns int64
|
||||
++
|
||||
+ delims render.Delims
|
||||
+ secureJSONPrefix string
|
||||
+ HTMLRender render.HTMLRender
|
||||
+@@ -534,6 +538,17 @@ func parseIP(ip string) net.IP {
|
||||
+ return parsedIP
|
||||
+ }
|
||||
+
|
||||
++// wrapListener wraps a net.Listener with connection limiting if MaxConns > 0.
|
||||
++func (engine *Engine) wrapListener(ln net.Listener) net.Listener {
|
||||
++ if engine.MaxConns <= 0 {
|
||||
++ return ln
|
||||
++ }
|
||||
++ return &limitedListener{
|
||||
++ Listener: ln,
|
||||
++ sem: make(chan struct{}, engine.MaxConns),
|
||||
++ }
|
||||
++}
|
||||
++
|
||||
+ // Run attaches the router to a http.Server and starts listening and serving HTTP requests.
|
||||
+ // It is a shortcut for http.ListenAndServe(addr, router)
|
||||
+ // Note: this method will block the calling goroutine indefinitely unless an error happens.
|
||||
+@@ -547,11 +562,15 @@ func (engine *Engine) Run(addr ...string) (err error) {
|
||||
+ engine.updateRouteTrees()
|
||||
+ address := resolveAddress(addr)
|
||||
+ debugPrint("Listening and serving HTTP on %s\n", address)
|
||||
++ ln, err := net.Listen("tcp", address)
|
||||
++ if err != nil {
|
||||
++ return
|
||||
++ }
|
||||
+ server := &http.Server{ // #nosec G112
|
||||
+ Addr: address,
|
||||
+ Handler: engine.Handler(),
|
||||
+ }
|
||||
+- err = server.ListenAndServe()
|
||||
++ err = server.Serve(engine.wrapListener(ln))
|
||||
+ return
|
||||
+ }
|
||||
+
|
||||
+@@ -571,7 +590,11 @@ func (engine *Engine) RunTLS(addr, certFile, keyFile string) (err error) {
|
||||
+ Addr: addr,
|
||||
+ Handler: engine.Handler(),
|
||||
+ }
|
||||
+- err = server.ListenAndServeTLS(certFile, keyFile)
|
||||
++ ln, err := net.Listen("tcp", addr)
|
||||
++ if err != nil {
|
||||
++ return
|
||||
++ }
|
||||
++ err = server.ServeTLS(engine.wrapListener(ln), certFile, keyFile)
|
||||
+ return
|
||||
+ }
|
||||
+
|
||||
+@@ -597,7 +620,7 @@ func (engine *Engine) RunUnix(file string) (err error) {
|
||||
+ server := &http.Server{ // #nosec G112
|
||||
+ Handler: engine.Handler(),
|
||||
+ }
|
||||
+- err = server.Serve(listener)
|
||||
++ err = server.Serve(engine.wrapListener(listener))
|
||||
+ return
|
||||
+ }
|
||||
+
|
||||
+@@ -627,6 +650,7 @@ func (engine *Engine) RunFd(fd int) (err error) {
|
||||
+ // RunQUIC attaches the router to a http.Server and starts listening and serving QUIC requests.
|
||||
+ // It is a shortcut for http3.ListenAndServeQUIC(addr, certFile, keyFile, router)
|
||||
+ // Note: this method will block the calling goroutine indefinitely unless an error happens.
|
||||
++// Note: MaxConns is not supported for QUIC due to protocol limitations.
|
||||
+ func (engine *Engine) RunQUIC(addr, certFile, keyFile string) (err error) {
|
||||
+ debugPrint("Listening and serving QUIC on %s\n", addr)
|
||||
+ defer func() { debugPrintError(err) }()
|
||||
+@@ -654,7 +678,7 @@ func (engine *Engine) RunListener(listener net.Listener) (err error) {
|
||||
+ server := &http.Server{ // #nosec G112
|
||||
+ Handler: engine.Handler(),
|
||||
+ }
|
||||
+- err = server.Serve(listener)
|
||||
++ err = server.Serve(engine.wrapListener(listener))
|
||||
+ return
|
||||
+ }
|
||||
+
|
||||
diff --git a/config.go b/config.go
|
||||
new file mode 100644
|
||||
index 0000000..232163c
|
||||
--- /dev/null
|
||||
+++ b/config.go
|
||||
@@ -0,0 +1,45 @@
|
||||
+// Copyright 2025 Gin Core Team. All rights reserved.
|
||||
+// Use of this source code is governed by a MIT style
|
||||
+// license that can be found in the LICENSE file.
|
||||
+
|
||||
+package gin
|
||||
+
|
||||
+import (
|
||||
+ "os"
|
||||
+
|
||||
+ "github.com/goccy/go-yaml"
|
||||
+)
|
||||
+
|
||||
+// Config represents the YAML configuration structure for Gin.
|
||||
+type Config struct {
|
||||
+ // MaxConns limits the maximum number of concurrent connections.
|
||||
+ // 0 means no limit (default behavior).
|
||||
+ MaxConns int64 `yaml:"max_conns"`
|
||||
+}
|
||||
+
|
||||
+// LoadConfig reads configuration from a YAML file and returns an OptionFunc
|
||||
+// that applies the configuration to an Engine.
|
||||
+func LoadConfig(path string) (OptionFunc, error) {
|
||||
+ data, err := os.ReadFile(path)
|
||||
+ if err != nil {
|
||||
+ return nil, err
|
||||
+ }
|
||||
+
|
||||
+ var cfg Config
|
||||
+ if err := yaml.Unmarshal(data, &cfg); err != nil {
|
||||
+ return nil, err
|
||||
+ }
|
||||
+
|
||||
+ return func(e *Engine) {
|
||||
+ if cfg.MaxConns > 0 {
|
||||
+ e.MaxConns = cfg.MaxConns
|
||||
+ }
|
||||
+ }, nil
|
||||
+}
|
||||
+
|
||||
+// WithMaxConns creates an OptionFunc that sets the maximum concurrent connections.
|
||||
+func WithMaxConns(n int64) OptionFunc {
|
||||
+ return func(e *Engine) {
|
||||
+ e.MaxConns = n
|
||||
+ }
|
||||
+}
|
||||
diff --git a/config_test.go b/config_test.go
|
||||
new file mode 100644
|
||||
index 0000000..6a65fe5
|
||||
--- /dev/null
|
||||
+++ b/config_test.go
|
||||
@@ -0,0 +1,72 @@
|
||||
+// Copyright 2025 Gin Core Team. All rights reserved.
|
||||
+// Use of this source code is governed by a MIT style
|
||||
+// license that can be found in the LICENSE file.
|
||||
+
|
||||
+package gin
|
||||
+
|
||||
+import (
|
||||
+ "os"
|
||||
+ "path/filepath"
|
||||
+ "testing"
|
||||
+
|
||||
+ "github.com/stretchr/testify/assert"
|
||||
+ "github.com/stretchr/testify/require"
|
||||
+)
|
||||
+
|
||||
+func TestLoadConfig(t *testing.T) {
|
||||
+ path := filepath.Join("testdata", "config.yaml")
|
||||
+ opt, err := LoadConfig(path)
|
||||
+ require.NoError(t, err)
|
||||
+ require.NotNil(t, opt)
|
||||
+
|
||||
+ engine := New()
|
||||
+ opt(engine)
|
||||
+ assert.Equal(t, int64(100), engine.MaxConns)
|
||||
+}
|
||||
+
|
||||
+func TestLoadConfigNotFound(t *testing.T) {
|
||||
+ opt, err := LoadConfig("nonexistent.yaml")
|
||||
+ assert.Error(t, err)
|
||||
+ assert.Nil(t, opt)
|
||||
+}
|
||||
+
|
||||
+func TestLoadConfigInvalidYAML(t *testing.T) {
|
||||
+ // Create a temporary file with invalid YAML
|
||||
+ tmpFile, err := os.CreateTemp("", "invalid_*.yaml")
|
||||
+ require.NoError(t, err)
|
||||
+ defer os.Remove(tmpFile.Name())
|
||||
+
|
||||
+ _, err = tmpFile.WriteString("max_conns: [invalid")
|
||||
+ require.NoError(t, err)
|
||||
+ tmpFile.Close()
|
||||
+
|
||||
+ opt, err := LoadConfig(tmpFile.Name())
|
||||
+ assert.Error(t, err)
|
||||
+ assert.Nil(t, opt)
|
||||
+}
|
||||
+
|
||||
+func TestLoadConfigEmpty(t *testing.T) {
|
||||
+ // Create a temporary empty config file
|
||||
+ tmpFile, err := os.CreateTemp("", "empty_*.yaml")
|
||||
+ require.NoError(t, err)
|
||||
+ defer os.Remove(tmpFile.Name())
|
||||
+ tmpFile.Close()
|
||||
+
|
||||
+ opt, err := LoadConfig(tmpFile.Name())
|
||||
+ require.NoError(t, err)
|
||||
+ require.NotNil(t, opt)
|
||||
+
|
||||
+ engine := New()
|
||||
+ opt(engine)
|
||||
+ assert.Equal(t, int64(0), engine.MaxConns)
|
||||
+}
|
||||
+
|
||||
+func TestWithMaxConns(t *testing.T) {
|
||||
+ engine := New(WithMaxConns(50))
|
||||
+ assert.Equal(t, int64(50), engine.MaxConns)
|
||||
+}
|
||||
+
|
||||
+func TestWithMaxConnsZero(t *testing.T) {
|
||||
+ engine := New(WithMaxConns(0))
|
||||
+ assert.Equal(t, int64(0), engine.MaxConns)
|
||||
+}
|
||||
diff --git a/gin.go b/gin.go
|
||||
index 2e033bf..2ccc093 100644
|
||||
--- a/gin.go
|
||||
+++ b/gin.go
|
||||
@@ -172,6 +172,10 @@ type Engine struct {
|
||||
// ContextWithFallback enable fallback Context.Deadline(), Context.Done(), Context.Err() and Context.Value() when Context.Request.Context() is not nil.
|
||||
ContextWithFallback bool
|
||||
|
||||
+ // MaxConns limits the maximum number of concurrent connections.
|
||||
+ // 0 means no limit (default behavior).
|
||||
+ MaxConns int64
|
||||
+
|
||||
delims render.Delims
|
||||
secureJSONPrefix string
|
||||
HTMLRender render.HTMLRender
|
||||
@@ -534,6 +538,17 @@ func parseIP(ip string) net.IP {
|
||||
return parsedIP
|
||||
}
|
||||
|
||||
+// wrapListener wraps a net.Listener with connection limiting if MaxConns > 0.
|
||||
+func (engine *Engine) wrapListener(ln net.Listener) net.Listener {
|
||||
+ if engine.MaxConns <= 0 {
|
||||
+ return ln
|
||||
+ }
|
||||
+ return &limitedListener{
|
||||
+ Listener: ln,
|
||||
+ sem: make(chan struct{}, engine.MaxConns),
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
// Run attaches the router to a http.Server and starts listening and serving HTTP requests.
|
||||
// It is a shortcut for http.ListenAndServe(addr, router)
|
||||
// Note: this method will block the calling goroutine indefinitely unless an error happens.
|
||||
@@ -547,11 +562,15 @@ func (engine *Engine) Run(addr ...string) (err error) {
|
||||
engine.updateRouteTrees()
|
||||
address := resolveAddress(addr)
|
||||
debugPrint("Listening and serving HTTP on %s\n", address)
|
||||
+ ln, err := net.Listen("tcp", address)
|
||||
+ if err != nil {
|
||||
+ return
|
||||
+ }
|
||||
server := &http.Server{ // #nosec G112
|
||||
Addr: address,
|
||||
Handler: engine.Handler(),
|
||||
}
|
||||
- err = server.ListenAndServe()
|
||||
+ err = server.Serve(engine.wrapListener(ln))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -571,7 +590,11 @@ func (engine *Engine) RunTLS(addr, certFile, keyFile string) (err error) {
|
||||
Addr: addr,
|
||||
Handler: engine.Handler(),
|
||||
}
|
||||
- err = server.ListenAndServeTLS(certFile, keyFile)
|
||||
+ ln, err := net.Listen("tcp", addr)
|
||||
+ if err != nil {
|
||||
+ return
|
||||
+ }
|
||||
+ err = server.ServeTLS(engine.wrapListener(ln), certFile, keyFile)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -597,7 +620,7 @@ func (engine *Engine) RunUnix(file string) (err error) {
|
||||
server := &http.Server{ // #nosec G112
|
||||
Handler: engine.Handler(),
|
||||
}
|
||||
- err = server.Serve(listener)
|
||||
+ err = server.Serve(engine.wrapListener(listener))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -627,6 +650,7 @@ func (engine *Engine) RunFd(fd int) (err error) {
|
||||
// RunQUIC attaches the router to a http.Server and starts listening and serving QUIC requests.
|
||||
// It is a shortcut for http3.ListenAndServeQUIC(addr, certFile, keyFile, router)
|
||||
// Note: this method will block the calling goroutine indefinitely unless an error happens.
|
||||
+// Note: MaxConns is not supported for QUIC due to protocol limitations.
|
||||
func (engine *Engine) RunQUIC(addr, certFile, keyFile string) (err error) {
|
||||
debugPrint("Listening and serving QUIC on %s\n", addr)
|
||||
defer func() { debugPrintError(err) }()
|
||||
@@ -654,7 +678,7 @@ func (engine *Engine) RunListener(listener net.Listener) (err error) {
|
||||
server := &http.Server{ // #nosec G112
|
||||
Handler: engine.Handler(),
|
||||
}
|
||||
- err = server.Serve(listener)
|
||||
+ err = server.Serve(engine.wrapListener(listener))
|
||||
return
|
||||
}
|
||||
|
||||
diff --git a/listener.go b/listener.go
|
||||
new file mode 100644
|
||||
index 0000000..b31679a
|
||||
--- /dev/null
|
||||
+++ b/listener.go
|
||||
@@ -0,0 +1,47 @@
|
||||
+// Copyright 2025 Gin Core Team. All rights reserved.
|
||||
+// Use of this source code is governed by a MIT style
|
||||
+// license that can be found in the LICENSE file.
|
||||
+
|
||||
+package gin
|
||||
+
|
||||
+import (
|
||||
+ "net"
|
||||
+ "sync"
|
||||
+)
|
||||
+
|
||||
+// limitedListener wraps a net.Listener and limits the number of concurrent connections
|
||||
+// using a buffered channel as a semaphore.
|
||||
+type limitedListener struct {
|
||||
+ net.Listener
|
||||
+ sem chan struct{}
|
||||
+}
|
||||
+
|
||||
+// Accept accepts a new connection. If the connection limit has been reached,
|
||||
+// the new connection is immediately closed.
|
||||
+func (l *limitedListener) Accept() (net.Conn, error) {
|
||||
+ conn, err := l.Listener.Accept()
|
||||
+ if err != nil {
|
||||
+ return nil, err
|
||||
+ }
|
||||
+
|
||||
+ select {
|
||||
+ case l.sem <- struct{}{}:
|
||||
+ return &limitedConn{Conn: conn, sem: l.sem}, nil
|
||||
+ default:
|
||||
+ conn.Close()
|
||||
+ return nil, nil
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+// limitedConn wraps a net.Conn and releases the semaphore slot on Close.
|
||||
+type limitedConn struct {
|
||||
+ net.Conn
|
||||
+ sem chan struct{}
|
||||
+ closeOnce sync.Once
|
||||
+}
|
||||
+
|
||||
+// Close closes the connection and releases the semaphore slot.
|
||||
+func (c *limitedConn) Close() error {
|
||||
+ c.closeOnce.Do(func() { <-c.sem })
|
||||
+ return c.Conn.Close()
|
||||
+}
|
||||
diff --git a/listener_test.go b/listener_test.go
|
||||
new file mode 100644
|
||||
index 0000000..32ce702
|
||||
--- /dev/null
|
||||
+++ b/listener_test.go
|
||||
@@ -0,0 +1,163 @@
|
||||
+// Copyright 2025 Gin Core Team. All rights reserved.
|
||||
+// Use of this source code is governed by a MIT style
|
||||
+// license that can be found in the LICENSE file.
|
||||
+
|
||||
+package gin
|
||||
+
|
||||
+import (
|
||||
+ "net"
|
||||
+ "net/http"
|
||||
+ "net/http/httptest"
|
||||
+ "sync"
|
||||
+ "sync/atomic"
|
||||
+ "testing"
|
||||
+ "time"
|
||||
+
|
||||
+ "github.com/stretchr/testify/assert"
|
||||
+ "github.com/stretchr/testify/require"
|
||||
+)
|
||||
+
|
||||
+func TestLimitedListenerUnderLimit(t *testing.T) {
|
||||
+ router := New(WithMaxConns(10))
|
||||
+ router.GET("/", func(c *Context) {
|
||||
+ c.String(http.StatusOK, "ok")
|
||||
+ })
|
||||
+
|
||||
+ server := httptest.NewServer(router.Handler())
|
||||
+ defer server.Close()
|
||||
+
|
||||
+ // Should be able to make requests under limit
|
||||
+ resp, err := http.Get(server.URL)
|
||||
+ require.NoError(t, err)
|
||||
+ assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
+ resp.Body.Close()
|
||||
+}
|
||||
+
|
||||
+func TestLimitedListenerAtLimit(t *testing.T) {
|
||||
+ // Create a server that holds connections
|
||||
+ var activeConns atomic.Int32
|
||||
+ var wg sync.WaitGroup
|
||||
+ block := make(chan struct{})
|
||||
+
|
||||
+ router := New(WithMaxConns(2))
|
||||
+ router.GET("/", func(c *Context) {
|
||||
+ activeConns.Add(1)
|
||||
+ wg.Add(1)
|
||||
+ <-block // Block until test releases
|
||||
+ activeConns.Add(-1)
|
||||
+ wg.Done()
|
||||
+ c.String(http.StatusOK, "ok")
|
||||
+ })
|
||||
+
|
||||
+ server := httptest.NewServer(router.Handler())
|
||||
+ defer server.Close()
|
||||
+
|
||||
+ // Start 2 requests that will block
|
||||
+ for i := 0; i < 2; i++ {
|
||||
+ go func() {
|
||||
+ resp, err := http.Get(server.URL)
|
||||
+ if err == nil {
|
||||
+ resp.Body.Close()
|
||||
+ }
|
||||
+ }()
|
||||
+ }
|
||||
+
|
||||
+ // Wait for both connections to be active
|
||||
+ require.Eventually(t, func() bool {
|
||||
+ return activeConns.Load() == 2
|
||||
+ }, 2*time.Second, 10*time.Millisecond)
|
||||
+
|
||||
+ // Third request should be rejected immediately
|
||||
+ client := &http.Client{Timeout: 500 * time.Millisecond}
|
||||
+ _, err := client.Get(server.URL)
|
||||
+ // Connection should be rejected
|
||||
+ assert.Error(t, err, "expected connection to be rejected")
|
||||
+
|
||||
+ // Release the blocked connections
|
||||
+ close(block)
|
||||
+ wg.Wait()
|
||||
+}
|
||||
+
|
||||
+func TestLimitedConnRelease(t *testing.T) {
|
||||
+ block := make(chan struct{})
|
||||
+
|
||||
+ router := New(WithMaxConns(1))
|
||||
+ router.GET("/", func(c *Context) {
|
||||
+ <-block
|
||||
+ c.String(http.StatusOK, "ok")
|
||||
+ })
|
||||
+
|
||||
+ server := httptest.NewServer(router.Handler())
|
||||
+ defer server.Close()
|
||||
+
|
||||
+ // Start one blocking request
|
||||
+ var firstDone atomic.Bool
|
||||
+ go func() {
|
||||
+ resp, err := http.Get(server.URL)
|
||||
+ if err == nil {
|
||||
+ resp.Body.Close()
|
||||
+ firstDone.Store(true)
|
||||
+ }
|
||||
+ }()
|
||||
+
|
||||
+ // Give the first request time to start
|
||||
+ time.Sleep(100 * time.Millisecond)
|
||||
+
|
||||
+ // Second request should fail
|
||||
+ client := &http.Client{Timeout: 200 * time.Millisecond}
|
||||
+ _, err := client.Get(server.URL)
|
||||
+ assert.Error(t, err, "expected connection to be rejected when limit reached")
|
||||
+
|
||||
+ // Release the first connection
|
||||
+ close(block)
|
||||
+
|
||||
+ // Eventually first request should complete
|
||||
+ require.Eventually(t, firstDone.Load, 2*time.Second, 10*time.Millisecond)
|
||||
+}
|
||||
+
|
||||
+func TestLimitedListenerZeroLimit(t *testing.T) {
|
||||
+ router := New(WithMaxConns(0))
|
||||
+ router.GET("/", func(c *Context) {
|
||||
+ c.String(http.StatusOK, "ok")
|
||||
+ })
|
||||
+
|
||||
+ server := httptest.NewServer(router.Handler())
|
||||
+ defer server.Close()
|
||||
+
|
||||
+ // Should allow unlimited connections (zero means no limit)
|
||||
+ for i := 0; i < 5; i++ {
|
||||
+ resp, err := http.Get(server.URL)
|
||||
+ require.NoError(t, err)
|
||||
+ assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
+ resp.Body.Close()
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+func TestWrapListenerNoLimit(t *testing.T) {
|
||||
+ engine := New()
|
||||
+ assert.Equal(t, int64(0), engine.MaxConns)
|
||||
+
|
||||
+ // Create a dummy listener
|
||||
+ ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
+ require.NoError(t, err)
|
||||
+ defer ln.Close()
|
||||
+
|
||||
+ // Should return original listener when no limit
|
||||
+ wrapped := engine.wrapListener(ln)
|
||||
+ assert.Equal(t, ln, wrapped)
|
||||
+}
|
||||
+
|
||||
+func TestWrapListenerWithLimit(t *testing.T) {
|
||||
+ engine := New(WithMaxConns(5))
|
||||
+
|
||||
+ // Create a dummy listener
|
||||
+ ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
+ require.NoError(t, err)
|
||||
+ defer ln.Close()
|
||||
+
|
||||
+ // Should return wrapped listener
|
||||
+ wrapped := engine.wrapListener(ln)
|
||||
+ assert.NotNil(t, wrapped)
|
||||
+ _, ok := wrapped.(*limitedListener)
|
||||
+ assert.True(t, ok)
|
||||
+}
|
||||
diff --git a/my_changes.patch b/my_changes.patch
|
||||
new file mode 100644
|
||||
index 0000000..be3867b
|
||||
--- /dev/null
|
||||
+++ b/my_changes.patch
|
||||
@@ -0,0 +1,89 @@
|
||||
+diff --git a/gin.go b/gin.go
|
||||
+index 2e033bf..2ccc093 100644
|
||||
+--- a/gin.go
|
||||
++++ b/gin.go
|
||||
+@@ -172,6 +172,10 @@ type Engine struct {
|
||||
+ // ContextWithFallback enable fallback Context.Deadline(), Context.Done(), Context.Err() and Context.Value() when Context.Request.Context() is not nil.
|
||||
+ ContextWithFallback bool
|
||||
+
|
||||
++ // MaxConns limits the maximum number of concurrent connections.
|
||||
++ // 0 means no limit (default behavior).
|
||||
++ MaxConns int64
|
||||
++
|
||||
+ delims render.Delims
|
||||
+ secureJSONPrefix string
|
||||
+ HTMLRender render.HTMLRender
|
||||
+@@ -534,6 +538,17 @@ func parseIP(ip string) net.IP {
|
||||
+ return parsedIP
|
||||
+ }
|
||||
+
|
||||
++// wrapListener wraps a net.Listener with connection limiting if MaxConns > 0.
|
||||
++func (engine *Engine) wrapListener(ln net.Listener) net.Listener {
|
||||
++ if engine.MaxConns <= 0 {
|
||||
++ return ln
|
||||
++ }
|
||||
++ return &limitedListener{
|
||||
++ Listener: ln,
|
||||
++ sem: make(chan struct{}, engine.MaxConns),
|
||||
++ }
|
||||
++}
|
||||
++
|
||||
+ // Run attaches the router to a http.Server and starts listening and serving HTTP requests.
|
||||
+ // It is a shortcut for http.ListenAndServe(addr, router)
|
||||
+ // Note: this method will block the calling goroutine indefinitely unless an error happens.
|
||||
+@@ -547,11 +562,15 @@ func (engine *Engine) Run(addr ...string) (err error) {
|
||||
+ engine.updateRouteTrees()
|
||||
+ address := resolveAddress(addr)
|
||||
+ debugPrint("Listening and serving HTTP on %s\n", address)
|
||||
++ ln, err := net.Listen("tcp", address)
|
||||
++ if err != nil {
|
||||
++ return
|
||||
++ }
|
||||
+ server := &http.Server{ // #nosec G112
|
||||
+ Addr: address,
|
||||
+ Handler: engine.Handler(),
|
||||
+ }
|
||||
+- err = server.ListenAndServe()
|
||||
++ err = server.Serve(engine.wrapListener(ln))
|
||||
+ return
|
||||
+ }
|
||||
+
|
||||
+@@ -571,7 +590,11 @@ func (engine *Engine) RunTLS(addr, certFile, keyFile string) (err error) {
|
||||
+ Addr: addr,
|
||||
+ Handler: engine.Handler(),
|
||||
+ }
|
||||
+- err = server.ListenAndServeTLS(certFile, keyFile)
|
||||
++ ln, err := net.Listen("tcp", addr)
|
||||
++ if err != nil {
|
||||
++ return
|
||||
++ }
|
||||
++ err = server.ServeTLS(engine.wrapListener(ln), certFile, keyFile)
|
||||
+ return
|
||||
+ }
|
||||
+
|
||||
+@@ -597,7 +620,7 @@ func (engine *Engine) RunUnix(file string) (err error) {
|
||||
+ server := &http.Server{ // #nosec G112
|
||||
+ Handler: engine.Handler(),
|
||||
+ }
|
||||
+- err = server.Serve(listener)
|
||||
++ err = server.Serve(engine.wrapListener(listener))
|
||||
+ return
|
||||
+ }
|
||||
+
|
||||
+@@ -627,6 +650,7 @@ func (engine *Engine) RunFd(fd int) (err error) {
|
||||
+ // RunQUIC attaches the router to a http.Server and starts listening and serving QUIC requests.
|
||||
+ // It is a shortcut for http3.ListenAndServeQUIC(addr, certFile, keyFile, router)
|
||||
+ // Note: this method will block the calling goroutine indefinitely unless an error happens.
|
||||
++// Note: MaxConns is not supported for QUIC due to protocol limitations.
|
||||
+ func (engine *Engine) RunQUIC(addr, certFile, keyFile string) (err error) {
|
||||
+ debugPrint("Listening and serving QUIC on %s\n", addr)
|
||||
+ defer func() { debugPrintError(err) }()
|
||||
+@@ -654,7 +678,7 @@ func (engine *Engine) RunListener(listener net.Listener) (err error) {
|
||||
+ server := &http.Server{ // #nosec G112
|
||||
+ Handler: engine.Handler(),
|
||||
+ }
|
||||
+- err = server.Serve(listener)
|
||||
++ err = server.Serve(engine.wrapListener(listener))
|
||||
+ return
|
||||
+ }
|
||||
+
|
||||
diff --git a/testdata/config.yaml b/testdata/config.yaml
|
||||
new file mode 100644
|
||||
index 0000000..b922394
|
||||
--- /dev/null
|
||||
+++ b/testdata/config.yaml
|
||||
@@ -0,0 +1 @@
|
||||
+max_conns: 100
|
||||
1
testdata/config.yaml
vendored
Normal file
1
testdata/config.yaml
vendored
Normal file
@ -0,0 +1 @@
|
||||
max_conns: 100
|
||||
Loading…
x
Reference in New Issue
Block a user