Repository: zeromicro/go-zero
Pull request #3908 (2024-02-12 03:22:00)

optimize: shedding algorithm performance

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/zeromicro/go-zero/pull/3908?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zeromicro) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Comparison is base [(`609fb3d`)](https://app.codecov.io/gh/zeromicro/go-zero/commit/609fb3d59eb7e1be1d207493055f74bd844502fb?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zeromicro) 94.41% compared to head [(`c2a76c4`)](https://app.codecov.io/gh/zeromicro/go-zero/pull/3908?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zeromicro) 94.41%.

<details><summary>Additional details and impacted files</summary>



| [Files](https://app.codecov.io/gh/zeromicro/go-zero/pull/3908?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zeromicro) | Coverage Δ | |
|---|---|---|
| [core/load/adaptiveshedder.go](https://app.codecov.io/gh/zeromicro/go-zero/pull/3908?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zeromicro#diff-Y29yZS9sb2FkL2FkYXB0aXZlc2hlZGRlci5nbw==) | `100.00% <100.00%> (ø)` | |

</details>

Patch:
diff --git a/core/conf/readme.md b/core/conf/readme.md
index 7bd58540..c34a3b67 100644
--- a/core/conf/readme.md
+++ b/core/conf/readme.md
@@ -12,7 +12,7 @@ type RestfulConf struct {
 	MaxConns     int           `json:",default=10000"`
 	MaxBytes     int64         `json:",default=1048576"`
 	Timeout      time.Duration `json:",default=3s"`
-	CpuThreshold int64         `json:",default=900,range=[0:1000]"`
+	CpuThreshold int64         `json:",default=900,range=[0:1000)"`
 }
 ```
 
diff --git a/core/load/adaptiveshedder.go b/core/load/adaptiveshedder.go
index 4d1b7f1d..a7b6f49d 100644
--- a/core/load/adaptiveshedder.go
+++ b/core/load/adaptiveshedder.go
@@ -9,6 +9,7 @@ import (
 
 	"github.com/zeromicro/go-zero/core/collection"
 	"github.com/zeromicro/go-zero/core/logx"
+	"github.com/zeromicro/go-zero/core/mathx"
 	"github.com/zeromicro/go-zero/core/stat"
 	"github.com/zeromicro/go-zero/core/syncx"
 	"github.com/zeromicro/go-zero/core/timex"
@@ -21,8 +22,11 @@ const (
 	defaultCpuThreshold = 900
 	defaultMinRt        = float64(time.Second / time.Millisecond)
 	// moving average hyperparameter beta for calculating requests on the fly
-	flyingBeta      = 0.9
-	coolOffDuration = time.Second
+	flyingBeta               = 0.9
+	coolOffDuration          = time.Second
+	cpuMax                   = 1000 // millicpu
+	millisecondsPerSecond    = 1000
+	overloadFactorLowerBound = 0.1
 )
 
 var (
@@ -66,7 +70,7 @@ type (
 
 	adaptiveShedder struct {
 		cpuThreshold    int64
-		windows         int64
+		windowScale     float64
 		flying          int64
 		avgFlying       float64
 		avgFlyingLock   syncx.SpinLock
@@ -105,7 +109,7 @@ func NewAdaptiveShedder(opts ...ShedderOption) Shedder {
 	bucketDuration := options.window / time.Duration(options.buckets)
 	return &adaptiveShedder{
 		cpuThreshold:    options.cpuThreshold,
-		windows:         int64(time.Second / bucketDuration),
+		windowScale:     float64(time.Second) / float64(bucketDuration) / millisecondsPerSecond,
 		overloadTime:    syncx.NewAtomicDuration(),
 		droppedRecently: syncx.NewAtomicBool(),
 		passCounter: collection.NewRollingWindow(options.buckets, bucketDuration,
@@ -149,16 +153,17 @@ func (as *adaptiveShedder) highThru() bool {
 	as.avgFlyingLock.Lock()
 	avgFlying := as.avgFlying
 	as.avgFlyingLock.Unlock()
-	maxFlight := as.maxFlight()
-	return int64(avgFlying) > maxFlight && atomic.LoadInt64(&as.flying) > maxFlight
+	maxFlight := as.maxFlight() * as.overloadFactor()
+	return avgFlying > maxFlight && float64(atomic.LoadInt64(&as.flying)) > maxFlight
 }
 
-func (as *adaptiveShedder) maxFlight() int64 {
+func (as *adaptiveShedder) maxFlight() float64 {
 	// windows = buckets per second
 	// maxQPS = maxPASS * windows
 	// minRT = min average response time in milliseconds
-	// maxQPS * minRT / milliseconds_per_second
-	return int64(math.Max(1, float64(as.maxPass()*as.windows)*(as.minRt()/1e3)))
+	// allowedFlying = maxQPS * minRT / milliseconds_per_second
+	maxFlight := float64(as.maxPass()) * as.minRt() * as.windowScale
+	return mathx.AtLeast(maxFlight, 1)
 }
 
 func (as *adaptiveShedder) maxPass() int64 {
@@ -174,6 +179,8 @@ func (as *adaptiveShedder) maxPass() int64 {
 }
 
 func (as *adaptiveShedder) minRt() float64 {
+	// if no requests in previous windows, return defaultMinRt,
+	// its a reasonable large value to avoid dropping requests.
 	result := defaultMinRt
 
 	as.rtCounter.Reduce(func(b *collection.Bucket) {
@@ -190,6 +197,13 @@ func (as *adaptiveShedder) minRt() float64 {
 	return result
 }
 
+func (as *adaptiveShedder) overloadFactor() float64 {
+	// as.cpuThreshold must be less than cpuMax
+	factor := (cpuMax - float64(stat.CpuUsage())) / (cpuMax - float64(as.cpuThreshold))
+	// at least accept 10% of acceptable requests even cpu is highly overloaded.
+	return mathx.Between(factor, overloadFactorLowerBound, 1)
+}
+
 func (as *adaptiveShedder) shouldDrop() bool {
 	if as.systemOverloaded() || as.stillHot() {
 		if as.highThru() {
diff --git a/core/mathx/range.go b/core/mathx/range.go
new file mode 100644
index 00000000..d74ac4dd
--- /dev/null
+++ b/core/mathx/range.go
@@ -0,0 +1,34 @@
+package mathx
+
+type numerical interface {
+	~int | ~int8 | ~int16 | ~int32 | ~int64 |
+		~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 |
+		~float32 | ~float64
+}
+
+// AtLeast returns the greater of x or lower.
+func AtLeast[T numerical](x, lower T) T {
+	if x < lower {
+		return lower
+	}
+	return x
+}
+
+// AtMost returns the smaller of x or upper.
+func AtMost[T numerical](x, upper T) T {
+	if x > upper {
+		return upper
+	}
+	return x
+}
+
+// Between returns the value of x clamped to the range [lower, upper].
+func Between[T numerical](x, lower, upper T) T {
+	if x < lower {
+		return lower
+	}
+	if x > upper {
+		return upper
+	}
+	return x
+}
diff --git a/rest/config.go b/rest/config.go
index ece29bfd..eb5fdb0b 100644
--- a/rest/config.go
+++ b/rest/config.go
@@ -53,7 +53,7 @@ type (
 		MaxBytes int64  `json:",default=1048576"`
 		// milliseconds
 		Timeout      int64         `json:",default=3000"`
-		CpuThreshold int64         `json:",default=900,range=[0:1000]"`
+		CpuThreshold int64         `json:",default=900,range=[0:1000)"`
 		Signature    SignatureConf `json:",optional"`
 		// There are default values for all the items in Middlewares.
 		Middlewares MiddlewaresConf
diff --git a/rest/handler/prometheushandler.go b/rest/handler/prometheushandler.go
index 86676d4c..6abf4dee 100644
--- a/rest/handler/prometheushandler.go
+++ b/rest/handler/prometheushandler.go
@@ -18,7 +18,7 @@ var (
 		Name:      "duration_ms",
 		Help:      "http server requests duration(ms).",
 		Labels:    []string{"path", "method"},
-		Buckets:   []float64{5, 10, 25, 50, 100, 250, 500, 1000},
+		Buckets:   []float64{5, 10, 25, 50, 100, 250, 500, 750, 1000},
 	})
 
 	metricServerReqCodeTotal = metric.NewCounterVec(&metric.CounterVecOpts{
diff --git a/zrpc/config.go b/zrpc/config.go
index 84a32160..5e459a3c 100644
--- a/zrpc/config.go
+++ b/zrpc/config.go
@@ -43,7 +43,7 @@ type (
 		StrictControl bool               `json:",optional"`
 		// setting 0 means no timeout
 		Timeout      int64 `json:",default=2000"`
-		CpuThreshold int64 `json:",default=900,range=[0:1000]"`
+		CpuThreshold int64 `json:",default=900,range=[0:1000)"`
 		// grpc health check switch
 		Health      bool `json:",default=true"`
 		Middlewares ServerMiddlewaresConf


Test patch:
diff --git a/core/load/adaptiveshedder_test.go b/core/load/adaptiveshedder_test.go
index 94789322..104be20e 100644
--- a/core/load/adaptiveshedder_test.go
+++ b/core/load/adaptiveshedder_test.go
@@ -19,6 +19,7 @@ import (
 const (
 	buckets        = 10
 	bucketDuration = time.Millisecond * 50
+	windowFactor   = 0.01
 )
 
 func init() {
@@ -114,10 +115,10 @@ func TestAdaptiveShedderMaxFlight(t *testing.T) {
 	shedder := &adaptiveShedder{
 		passCounter:     passCounter,
 		rtCounter:       rtCounter,
-		windows:         buckets,
+		windowScale:     windowFactor,
 		droppedRecently: syncx.NewAtomicBool(),
 	}
-	assert.Equal(t, int64(54), shedder.maxFlight())
+	assert.Equal(t, float64(54), shedder.maxFlight())
 }
 
 func TestAdaptiveShedderShouldDrop(t *testing.T) {
@@ -136,7 +137,7 @@ func TestAdaptiveShedderShouldDrop(t *testing.T) {
 	shedder := &adaptiveShedder{
 		passCounter:     passCounter,
 		rtCounter:       rtCounter,
-		windows:         buckets,
+		windowScale:     windowFactor,
 		overloadTime:    syncx.NewAtomicDuration(),
 		droppedRecently: syncx.NewAtomicBool(),
 	}
@@ -149,7 +150,8 @@ func TestAdaptiveShedderShouldDrop(t *testing.T) {
 
 	// cpu >=  800, inflight > maxPass
 	shedder.avgFlying = 80
-	shedder.flying = 50
+	// because of the overloadFactor, so we need to make sure maxFlight is greater than flying
+	shedder.flying = int64(shedder.maxFlight()*shedder.overloadFactor()) - 5
 	assert.False(t, shedder.shouldDrop())
 
 	// cpu >=  800, inflight > maxPass
@@ -190,7 +192,7 @@ func TestAdaptiveShedderStillHot(t *testing.T) {
 	shedder := &adaptiveShedder{
 		passCounter:     passCounter,
 		rtCounter:       rtCounter,
-		windows:         buckets,
+		windowScale:     windowFactor,
 		overloadTime:    syncx.NewAtomicDuration(),
 		droppedRecently: syncx.ForAtomicBool(true),
 	}
@@ -239,6 +241,30 @@ func BenchmarkAdaptiveShedder_Allow(b *testing.B) {
 	b.Run("low load", bench)
 }
 
+func BenchmarkMaxFlight(b *testing.B) {
+	passCounter := newRollingWindow()
+	rtCounter := newRollingWindow()
+	for i := 0; i < 10; i++ {
+		if i > 0 {
+			time.Sleep(bucketDuration)
+		}
+		passCounter.Add(float64((i + 1) * 100))
+		for j := i*10 + 1; j <= i*10+10; j++ {
+			rtCounter.Add(float64(j))
+		}
+	}
+	shedder := &adaptiveShedder{
+		passCounter:     passCounter,
+		rtCounter:       rtCounter,
+		windowScale:     windowFactor,
+		droppedRecently: syncx.NewAtomicBool(),
+	}
+
+	for i := 0; i < b.N; i++ {
+		_ = shedder.maxFlight()
+	}
+}
+
 func newRollingWindow() *collection.RollingWindow {
 	return collection.NewRollingWindow(buckets, bucketDuration, collection.IgnoreCurrentBucket())
 }
diff --git a/core/mathx/range_test.go b/core/mathx/range_test.go
new file mode 100644
index 00000000..bff9c2bc
--- /dev/null
+++ b/core/mathx/range_test.go
@@ -0,0 +1,513 @@
+package mathx
+
+import "testing"
+
+func TestAtLeast(t *testing.T) {
+	t.Run("test int", func(t *testing.T) {
+		if got := AtLeast(10, 5); got != 10 {
+			t.Errorf("AtLeast() = %v, want 10", got)
+		}
+		if got := AtLeast(3, 5); got != 5 {
+			t.Errorf("AtLeast() = %v, want 5", got)
+		}
+		if got := AtLeast(5, 5); got != 5 {
+			t.Errorf("AtLeast() = %v, want 5", got)
+		}
+	})
+
+	t.Run("test int8", func(t *testing.T) {
+		if got := AtLeast(int8(10), int8(5)); got != 10 {
+			t.Errorf("AtLeast() = %v, want 10", got)
+		}
+		if got := AtLeast(int8(3), int8(5)); got != 5 {
+			t.Errorf("AtLeast() = %v, want 5", got)
+		}
+		if got := AtLeast(int8(5), int8(5)); got != 5 {
+			t.Errorf("AtLeast() = %v, want 5", got)
+		}
+	})
+
+	t.Run("test int16", func(t *testing.T) {
+		if got := AtLeast(int16(10), int16(5)); got != 10 {
+			t.Errorf("AtLeast() = %v, want 10", got)
+		}
+		if got := AtLeast(int16(3), int16(5)); got != 5 {
+			t.Errorf("AtLeast() = %v, want 5", got)
+		}
+		if got := AtLeast(int16(5), int16(5)); got != 5 {
+			t.Errorf("AtLeast() = %v, want 5", got)
+		}
+	})
+
+	t.Run("test int32", func(t *testing.T) {
+		if got := AtLeast(int32(10), int32(5)); got != 10 {
+			t.Errorf("AtLeast() = %v, want 10", got)
+		}
+		if got := AtLeast(int32(3), int32(5)); got != 5 {
+			t.Errorf("AtLeast() = %v, want 5", got)
+		}
+		if got := AtLeast(int32(5), int32(5)); got != 5 {
+			t.Errorf("AtLeast() = %v, want 5", got)
+		}
+	})
+
+	t.Run("test int64", func(t *testing.T) {
+		if got := AtLeast(int64(10), int64(5)); got != 10 {
+			t.Errorf("AtLeast() = %v, want 10", got)
+		}
+		if got := AtLeast(int64(3), int64(5)); got != 5 {
+			t.Errorf("AtLeast() = %v, want 5", got)
+		}
+		if got := AtLeast(int64(5), int64(5)); got != 5 {
+			t.Errorf("AtLeast() = %v, want 5", got)
+		}
+	})
+
+	t.Run("test uint", func(t *testing.T) {
+		if got := AtLeast(uint(10), uint(5)); got != 10 {
+			t.Errorf("AtLeast() = %v, want 10", got)
+		}
+		if got := AtLeast(uint(3), uint(5)); got != 5 {
+			t.Errorf("AtLeast() = %v, want 5", got)
+		}
+		if got := AtLeast(uint(5), uint(5)); got != 5 {
+			t.Errorf("AtLeast() = %v, want 5", got)
+		}
+	})
+
+	t.Run("test uint8", func(t *testing.T) {
+		if got := AtLeast(uint8(10), uint8(5)); got != 10 {
+			t.Errorf("AtLeast() = %v, want 10", got)
+		}
+		if got := AtLeast(uint8(3), uint8(5)); got != 5 {
+			t.Errorf("AtLeast() = %v, want 5", got)
+		}
+		if got := AtLeast(uint8(5), uint8(5)); got != 5 {
+			t.Errorf("AtLeast() = %v, want 5", got)
+		}
+	})
+
+	t.Run("test uint16", func(t *testing.T) {
+		if got := AtLeast(uint16(10), uint16(5)); got != 10 {
+			t.Errorf("AtLeast() = %v, want 10", got)
+		}
+		if got := AtLeast(uint16(3), uint16(5)); got != 5 {
+			t.Errorf("AtLeast() = %v, want 5", got)
+		}
+		if got := AtLeast(uint16(5), uint16(5)); got != 5 {
+			t.Errorf("AtLeast() = %v, want 5", got)
+		}
+	})
+
+	t.Run("test uint32", func(t *testing.T) {
+		if got := AtLeast(uint32(10), uint32(5)); got != 10 {
+			t.Errorf("AtLeast() = %v, want 10", got)
+		}
+		if got := AtLeast(uint32(3), uint32(5)); got != 5 {
+			t.Errorf("AtLeast() = %v, want 5", got)
+		}
+		if got := AtLeast(uint32(5), uint32(5)); got != 5 {
+			t.Errorf("AtLeast() = %v, want 5", got)
+		}
+	})
+
+	t.Run("test uint64", func(t *testing.T) {
+		if got := AtLeast(uint64(10), uint64(5)); got != 10 {
+			t.Errorf("AtLeast() = %v, want 10", got)
+		}
+		if got := AtLeast(uint64(3), uint64(5)); got != 5 {
+			t.Errorf("AtLeast() = %v, want 5", got)
+		}
+		if got := AtLeast(uint64(5), uint64(5)); got != 5 {
+			t.Errorf("AtLeast() = %v, want 5", got)
+		}
+	})
+
+	t.Run("test float32", func(t *testing.T) {
+		if got := AtLeast(float32(10.0), float32(5.0)); got != 10.0 {
+			t.Errorf("AtLeast() = %v, want 10.0", got)
+		}
+		if got := AtLeast(float32(3.0), float32(5.0)); got != 5.0 {
+			t.Errorf("AtLeast() = %v, want 5.0", got)
+		}
+		if got := AtLeast(float32(5.0), float32(5.0)); got != 5.0 {
+			t.Errorf("AtLeast() = %v, want 5.0", got)
+		}
+	})
+
+	t.Run("test float64", func(t *testing.T) {
+		if got := AtLeast(10.0, 5.0); got != 10.0 {
+			t.Errorf("AtLeast() = %v, want 10.0", got)
+		}
+		if got := AtLeast(3.0, 5.0); got != 5.0 {
+			t.Errorf("AtLeast() = %v, want 5.0", got)
+		}
+		if got := AtLeast(5.0, 5.0); got != 5.0 {
+			t.Errorf("AtLeast() = %v, want 5.0", got)
+		}
+	})
+}
+
+func TestAtMost(t *testing.T) {
+	t.Run("test int", func(t *testing.T) {
+		if got := AtMost(10, 5); got != 5 {
+			t.Errorf("AtMost() = %v, want 5", got)
+		}
+		if got := AtMost(3, 5); got != 3 {
+			t.Errorf("AtMost() = %v, want 3", got)
+		}
+		if got := AtMost(5, 5); got != 5 {
+			t.Errorf("AtMost() = %v, want 5", got)
+		}
+	})
+
+	t.Run("test int8", func(t *testing.T) {
+		if got := AtMost(int8(10), int8(5)); got != 5 {
+			t.Errorf("AtMost() = %v, want 5", got)
+		}
+		if got := AtMost(int8(3), int8(5)); got != 3 {
+			t.Errorf("AtMost() = %v, want 3", got)
+		}
+		if got := AtMost(int8(5), int8(5)); got != 5 {
+			t.Errorf("AtMost() = %v, want 5", got)
+		}
+	})
+
+	t.Run("test int16", func(t *testing.T) {
+		if got := AtMost(int16(10), int16(5)); got != 5 {
+			t.Errorf("AtMost() = %v, want 5", got)
+		}
+		if got := AtMost(int16(3), int16(5)); got != 3 {
+			t.Errorf("AtMost() = %v, want 3", got)
+		}
+		if got := AtMost(int16(5), int16(5)); got != 5 {
+			t.Errorf("AtMost() = %v, want 5", got)
+		}
+	})
+
+	t.Run("test int32", func(t *testing.T) {
+		if got := AtMost(int32(10), int32(5)); got != 5 {
+			t.Errorf("AtMost() = %v, want 5", got)
+		}
+		if got := AtMost(int32(3), int32(5)); got != 3 {
+			t.Errorf("AtMost() = %v, want 3", got)
+		}
+		if got := AtMost(int32(5), int32(5)); got != 5 {
+			t.Errorf("AtMost() = %v, want 5", got)
+		}
+	})
+
+	t.Run("test int64", func(t *testing.T) {
+		if got := AtMost(int64(10), int64(5)); got != 5 {
+			t.Errorf("AtMost() = %v, want 5", got)
+		}
+		if got := AtMost(int64(3), int64(5)); got != 3 {
+			t.Errorf("AtMost() = %v, want 3", got)
+		}
+		if got := AtMost(int64(5), int64(5)); got != 5 {
+			t.Errorf("AtMost() = %v, want 5", got)
+		}
+	})
+
+	t.Run("test uint", func(t *testing.T) {
+		if got := AtMost(uint(10), uint(5)); got != 5 {
+			t.Errorf("AtMost() = %v, want 5", got)
+		}
+		if got := AtMost(uint(3), uint(5)); got != 3 {
+			t.Errorf("AtMost() = %v, want 3", got)
+		}
+		if got := AtMost(uint(5), uint(5)); got != 5 {
+			t.Errorf("AtMost() = %v, want 5", got)
+		}
+	})
+
+	t.Run("test uint8", func(t *testing.T) {
+		if got := AtMost(uint8(10), uint8(5)); got != 5 {
+			t.Errorf("AtMost() = %v, want 5", got)
+		}
+		if got := AtMost(uint8(3), uint8(5)); got != 3 {
+			t.Errorf("AtMost() = %v, want 3", got)
+		}
+		if got := AtMost(uint8(5), uint8(5)); got != 5 {
+			t.Errorf("AtMost() = %v, want 5", got)
+		}
+	})
+
+	t.Run("test uint16", func(t *testing.T) {
+		if got := AtMost(uint16(10), uint16(5)); got != 5 {
+			t.Errorf("AtMost() = %v, want 5", got)
+		}
+		if got := AtMost(uint16(3), uint16(5)); got != 3 {
+			t.Errorf("AtMost() = %v, want 3", got)
+		}
+		if got := AtMost(uint16(5), uint16(5)); got != 5 {
+			t.Errorf("AtMost() = %v, want 5", got)
+		}
+	})
+
+	t.Run("test uint32", func(t *testing.T) {
+		if got := AtMost(uint32(10), uint32(5)); got != 5 {
+			t.Errorf("AtMost() = %v, want 5", got)
+		}
+		if got := AtMost(uint32(3), uint32(5)); got != 3 {
+			t.Errorf("AtMost() = %v, want 3", got)
+		}
+		if got := AtMost(uint32(5), uint32(5)); got != 5 {
+			t.Errorf("AtMost() = %v, want 5", got)
+		}
+	})
+
+	t.Run("test uint64", func(t *testing.T) {
+		if got := AtMost(uint64(10), uint64(5)); got != 5 {
+			t.Errorf("AtMost() = %v, want 5", got)
+		}
+		if got := AtMost(uint64(3), uint64(5)); got != 3 {
+			t.Errorf("AtMost() = %v, want 3", got)
+		}
+		if got := AtMost(uint64(5), uint64(5)); got != 5 {
+			t.Errorf("AtMost() = %v, want 5", got)
+		}
+	})
+
+	t.Run("test float32", func(t *testing.T) {
+		if got := AtMost(float32(10.0), float32(5.0)); got != 5.0 {
+			t.Errorf("AtMost() = %v, want 5.0", got)
+		}
+		if got := AtMost(float32(3.0), float32(5.0)); got != 3.0 {
+			t.Errorf("AtMost() = %v, want 3.0", got)
+		}
+		if got := AtMost(float32(5.0), float32(5.0)); got != 5.0 {
+			t.Errorf("AtMost() = %v, want 5.0", got)
+		}
+	})
+
+	t.Run("test float64", func(t *testing.T) {
+		if got := AtMost(10.0, 5.0); got != 5.0 {
+			t.Errorf("AtMost() = %v, want 5.0", got)
+		}
+		if got := AtMost(3.0, 5.0); got != 3.0 {
+			t.Errorf("AtMost() = %v, want 3.0", got)
+		}
+		if got := AtMost(5.0, 5.0); got != 5.0 {
+			t.Errorf("AtMost() = %v, want 5.0", got)
+		}
+	})
+}
+
+func TestBetween(t *testing.T) {
+	t.Run("test int", func(t *testing.T) {
+		if got := Between(10, 5, 15); got != 10 {
+			t.Errorf("Between() = %v, want 10", got)
+		}
+		if got := Between(3, 5, 15); got != 5 {
+			t.Errorf("Between() = %v, want 5", got)
+		}
+		if got := Between(20, 5, 15); got != 15 {
+			t.Errorf("Between() = %v, want 15", got)
+		}
+		if got := Between(5, 5, 15); got != 5 {
+			t.Errorf("Between() = %v, want 5", got)
+		}
+		if got := Between(15, 5, 15); got != 15 {
+			t.Errorf("Between() = %v, want 15", got)
+		}
+	})
+
+	t.Run("test int8", func(t *testing.T) {
+		if got := Between(int8(10), int8(5), int8(15)); got != 10 {
+			t.Errorf("Between() = %v, want 10", got)
+		}
+		if got := Between(int8(3), int8(5), int8(15)); got != 5 {
+			t.Errorf("Between() = %v, want 5", got)
+		}
+		if got := Between(int8(20), int8(5), int8(15)); got != 15 {
+			t.Errorf("Between() = %v, want 15", got)
+		}
+		if got := Between(int8(5), int8(5), int8(15)); got != 5 {
+			t.Errorf("Between() = %v, want 5", got)
+		}
+		if got := Between(int8(15), int8(5), int8(15)); got != 15 {
+			t.Errorf("Between() = %v, want 15", got)
+		}
+	})
+
+	t.Run("test int16", func(t *testing.T) {
+		if got := Between(int16(10), int16(5), int16(15)); got != 10 {
+			t.Errorf("Between() = %v, want 10", got)
+		}
+		if got := Between(int16(3), int16(5), int16(15)); got != 5 {
+			t.Errorf("Between() = %v, want 5", got)
+		}
+		if got := Between(int16(20), int16(5), int16(15)); got != 15 {
+			t.Errorf("Between() = %v, want 15", got)
+		}
+		if got := Between(int16(5), int16(5), int16(15)); got != 5 {
+			t.Errorf("Between() = %v, want 5", got)
+		}
+		if got := Between(int16(15), int16(5), int16(15)); got != 15 {
+			t.Errorf("Between() = %v, want 15", got)
+		}
+	})
+
+	t.Run("test int32", func(t *testing.T) {
+		if got := Between(int32(10), int32(5), int32(15)); got != 10 {
+			t.Errorf("Between() = %v, want 10", got)
+		}
+		if got := Between(int32(3), int32(5), int32(15)); got != 5 {
+			t.Errorf("Between() = %v, want 5", got)
+		}
+		if got := Between(int32(20), int32(5), int32(15)); got != 15 {
+			t.Errorf("Between() = %v, want 15", got)
+		}
+		if got := Between(int32(5), int32(5), int32(15)); got != 5 {
+			t.Errorf("Between() = %v, want 5", got)
+		}
+		if got := Between(int32(15), int32(5), int32(15)); got != 15 {
+			t.Errorf("Between() = %v, want 15", got)
+		}
+	})
+
+	t.Run("test int64", func(t *testing.T) {
+		if got := Between(int64(10), int64(5), int64(15)); got != 10 {
+			t.Errorf("Between() = %v, want 10", got)
+		}
+		if got := Between(int64(3), int64(5), int64(15)); got != 5 {
+			t.Errorf("Between() = %v, want 5", got)
+		}
+		if got := Between(int64(20), int64(5), int64(15)); got != 15 {
+			t.Errorf("Between() = %v, want 15", got)
+		}
+		if got := Between(int64(5), int64(5), int64(15)); got != 5 {
+			t.Errorf("Between() = %v, want 5", got)
+		}
+		if got := Between(int64(15), int64(5), int64(15)); got != 15 {
+			t.Errorf("Between() = %v, want 15", got)
+		}
+	})
+
+	t.Run("test uint", func(t *testing.T) {
+		if got := Between(uint(10), uint(5), uint(15)); got != 10 {
+			t.Errorf("Between() = %v, want 10", got)
+		}
+		if got := Between(uint(3), uint(5), uint(15)); got != 5 {
+			t.Errorf("Between() = %v, want 5", got)
+		}
+		if got := Between(uint(20), uint(5), uint(15)); got != 15 {
+			t.Errorf("Between() = %v, want 15", got)
+		}
+		if got := Between(uint(5), uint(5), uint(15)); got != 5 {
+			t.Errorf("Between() = %v, want 5", got)
+		}
+		if got := Between(uint(15), uint(5), uint(15)); got != 15 {
+			t.Errorf("Between() = %v, want 15", got)
+		}
+	})
+
+	t.Run("test uint8", func(t *testing.T) {
+		if got := Between(uint8(10), uint8(5), uint8(15)); got != 10 {
+			t.Errorf("Between() = %v, want 10", got)
+		}
+		if got := Between(uint8(3), uint8(5), uint8(15)); got != 5 {
+			t.Errorf("Between() = %v, want 5", got)
+		}
+		if got := Between(uint8(20), uint8(5), uint8(15)); got != 15 {
+			t.Errorf("Between() = %v, want 15", got)
+		}
+		if got := Between(uint8(5), uint8(5), uint8(15)); got != 5 {
+			t.Errorf("Between() = %v, want 5", got)
+		}
+		if got := Between(uint8(15), uint8(5), uint8(15)); got != 15 {
+			t.Errorf("Between() = %v, want 15", got)
+		}
+	})
+
+	t.Run("test uint16", func(t *testing.T) {
+		if got := Between(uint16(10), uint16(5), uint16(15)); got != 10 {
+			t.Errorf("Between() = %v, want 10", got)
+		}
+		if got := Between(uint16(3), uint16(5), uint16(15)); got != 5 {
+			t.Errorf("Between() = %v, want 5", got)
+		}
+		if got := Between(uint16(20), uint16(5), uint16(15)); got != 15 {
+			t.Errorf("Between() = %v, want 15", got)
+		}
+		if got := Between(uint16(5), uint16(5), uint16(15)); got != 5 {
+			t.Errorf("Between() = %v, want 5", got)
+		}
+		if got := Between(uint16(15), uint16(5), uint16(15)); got != 15 {
+			t.Errorf("Between() = %v, want 15", got)
+		}
+	})
+
+	t.Run("test uint32", func(t *testing.T) {
+		if got := Between(uint32(10), uint32(5), uint32(15)); got != 10 {
+			t.Errorf("Between() = %v, want 10", got)
+		}
+		if got := Between(uint32(3), uint32(5), uint32(15)); got != 5 {
+			t.Errorf("Between() = %v, want 5", got)
+		}
+		if got := Between(uint32(20), uint32(5), uint32(15)); got != 15 {
+			t.Errorf("Between() = %v, want 15", got)
+		}
+		if got := Between(uint32(5), uint32(5), uint32(15)); got != 5 {
+			t.Errorf("Between() = %v, want 5", got)
+		}
+		if got := Between(uint32(15), uint32(5), uint32(15)); got != 15 {
+			t.Errorf("Between() = %v, want 15", got)
+		}
+	})
+
+	t.Run("test uint64", func(t *testing.T) {
+		if got := Between(uint64(10), uint64(5), uint64(15)); got != 10 {
+			t.Errorf("Between() = %v, want 10", got)
+		}
+		if got := Between(uint64(3), uint64(5), uint64(15)); got != 5 {
+			t.Errorf("Between() = %v, want 5", got)
+		}
+		if got := Between(uint64(20), uint64(5), uint64(15)); got != 15 {
+			t.Errorf("Between() = %v, want 15", got)
+		}
+		if got := Between(uint64(5), uint64(5), uint64(15)); got != 5 {
+			t.Errorf("Between() = %v, want 5", got)
+		}
+		if got := Between(uint64(15), uint64(5), uint64(15)); got != 15 {
+			t.Errorf("Between() = %v, want 15", got)
+		}
+	})
+
+	t.Run("test float32", func(t *testing.T) {
+		if got := Between(float32(10.0), float32(5.0), float32(15.0)); got != 10.0 {
+			t.Errorf("Between() = %v, want 10.0", got)
+		}
+		if got := Between(float32(3.0), float32(5.0), float32(15.0)); got != 5.0 {
+			t.Errorf("Between() = %v, want 5.0", got)
+		}
+		if got := Between(float32(20.0), float32(5.0), float32(15.0)); got != 15.0 {
+			t.Errorf("Between() = %v, want 15.0", got)
+		}
+		if got := Between(float32(5.0), float32(5.0), float32(15.0)); got != 5.0 {
+			t.Errorf("Between() = %v, want 5.0", got)
+		}
+		if got := Between(float32(15.0), float32(5.0), float32(15.0)); got != 15.0 {
+			t.Errorf("Between() = %v, want 15.0", got)
+		}
+	})
+
+	t.Run("test float64", func(t *testing.T) {
+		if got := Between(10.0, 5.0, 15.0); got != 10.0 {
+			t.Errorf("Between() = %v, want 10.0", got)
+		}
+		if got := Between(3.0, 5.0, 15.0); got != 5.0 {
+			t.Errorf("Between() = %v, want 5.0", got)
+		}
+		if got := Between(20.0, 5.0, 15.0); got != 15.0 {
+			t.Errorf("Between() = %v, want 15.0", got)
+		}
+		if got := Between(5.0, 5.0, 15.0); got != 5.0 {
+			t.Errorf("Between() = %v, want 5.0", got)
+		}
+		if got := Between(15.0, 5.0, 15.0); got != 15.0 {
+			t.Errorf("Between() = %v, want 15.0", got)
+		}
+	})
+}