-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathadaptive_concurrency.go
More file actions
172 lines (145 loc) · 4.34 KB
/
Copy pathadaptive_concurrency.go
File metadata and controls
172 lines (145 loc) · 4.34 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
// Copyright (c) 2026 Onur Cinar.
// The source code is provided under MIT License.
// https://github.com/cinar/resile
package resile
import (
"context"
"sync"
"sync/atomic"
"time"
)
// AdaptiveLimiter implements a dynamic concurrency limiter using Additive Increase,
// Multiplicative Decrease (AIMD) based on Round Trip Time (RTT), applying Little's Law.
type AdaptiveLimiter struct {
mu sync.RWMutex
// Atomic state for high-frequency checks
currentConcurrency int32
maxConcurrency int32
// Little's Law & AIMD state
minBaselineRTT time.Duration
rttEWMA time.Duration
// Configuration
minConcurrency int32
limitScaling float64
threshold float64
alpha float64 // EWMA weight
// Decay mechanism
lastDecayUpdate time.Time
decayInterval time.Duration
// Health channel for backpressure signaling
healthCh chan StateEvent
healthChInit bool
// Track previous max for change detection
prevMaxConcurrency int32
}
// NewAdaptiveLimiter creates a new AdaptiveLimiter with default configurations.
func NewAdaptiveLimiter() *AdaptiveLimiter {
return &AdaptiveLimiter{
maxConcurrency: 10, // Initial guess
prevMaxConcurrency: 10,
minConcurrency: 1, // Floor
limitScaling: 0.8, // Multiplicative Decrease factor
threshold: 1.5, // Threshold for increase/decrease
alpha: 0.1, // EWMA smoothing factor
decayInterval: time.Minute,
lastDecayUpdate: time.Now(),
}
}
// Execute wraps an action with adaptive concurrency control.
func (al *AdaptiveLimiter) Execute(ctx context.Context, action func() error) error {
// 1. Admission Control
current := atomic.AddInt32(&al.currentConcurrency, 1)
defer atomic.AddInt32(&al.currentConcurrency, -1)
max := atomic.LoadInt32(&al.maxConcurrency)
if current > max {
return ErrShedLoad
}
// 2. Measure RTT
start := time.Now()
err := action()
rtt := time.Since(start)
// 3. Update state
al.update(rtt)
return err
}
func (al *AdaptiveLimiter) update(rtt time.Duration) {
al.mu.Lock()
defer al.mu.Unlock()
// Handle initial state
if al.minBaselineRTT == 0 {
al.minBaselineRTT = rtt
al.rttEWMA = rtt
return
}
// Decay minBaselineRTT periodically to allow recalibration
now := time.Now()
if now.Sub(al.lastDecayUpdate) > al.decayInterval {
al.minBaselineRTT = time.Duration(float64(al.minBaselineRTT) * 1.05)
al.lastDecayUpdate = now
}
// Update minBaselineRTT if we found a new faster path
if rtt < al.minBaselineRTT {
al.minBaselineRTT = rtt
}
// Update EWMA
al.rttEWMA = time.Duration((1-al.alpha)*float64(al.rttEWMA) + al.alpha*float64(rtt))
// AIMD Logic
// Calculate queue delay threshold
if float64(al.rttEWMA) < float64(al.minBaselineRTT)*al.threshold {
// Additive Increase
atomic.AddInt32(&al.maxConcurrency, 1)
al.checkAndEmitEvent(false)
} else {
// Multiplicative Decrease
newMax := float64(atomic.LoadInt32(&al.maxConcurrency)) * al.limitScaling
if newMax < float64(al.minConcurrency) {
newMax = float64(al.minConcurrency)
}
atomic.StoreInt32(&al.maxConcurrency, int32(newMax))
al.checkAndEmitEvent(true)
}
}
func (al *AdaptiveLimiter) checkAndEmitEvent(decreased bool) {
currentMax := atomic.LoadInt32(&al.maxConcurrency)
prev := al.prevMaxConcurrency
if decreased {
threshold := float64(prev) * 0.5
if float64(currentMax) <= threshold {
al.emitStateEvent(HealthStateDegraded, "capacity decreased")
}
} else {
if currentMax > prev {
al.emitStateEvent(HealthStateHealthy, "capacity increased")
}
}
al.prevMaxConcurrency = currentMax
}
// GetMaxConcurrency returns the current maximum concurrency limit.
func (al *AdaptiveLimiter) GetMaxConcurrency() int32 {
return atomic.LoadInt32(&al.maxConcurrency)
}
// Health returns a channel that emits state change events.
// The channel is created lazily on first access.
// The caller should use a select with default to handle non-blocking receive.
func (al *AdaptiveLimiter) Health() <-chan StateEvent {
al.mu.Lock()
defer al.mu.Unlock()
if !al.healthChInit {
al.healthCh = make(chan StateEvent, 10)
al.healthChInit = true
}
return al.healthCh
}
func (al *AdaptiveLimiter) emitStateEvent(state HealthState, message string) {
if al.healthChInit {
select {
case al.healthCh <- StateEvent{
Component: "adaptive-limiter",
State: state,
Timestamp: time.Now(),
Message: message,
}:
default:
}
}
}