Skip to content

Commit 0bb3e4a

Browse files
author
QTom
committed
feat: add RPM getter methods and schedulability check to Account model
1 parent 9d79506 commit 0bb3e4a

2 files changed

Lines changed: 144 additions & 0 deletions

File tree

backend/internal/service/account.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1137,6 +1137,77 @@ func (a *Account) GetSessionIdleTimeoutMinutes() int {
11371137
return 5
11381138
}
11391139

1140+
// GetBaseRPM 获取基础 RPM 限制
1141+
// 返回 0 表示未启用
1142+
func (a *Account) GetBaseRPM() int {
1143+
if a.Extra == nil {
1144+
return 0
1145+
}
1146+
if v, ok := a.Extra["base_rpm"]; ok {
1147+
return parseExtraInt(v)
1148+
}
1149+
return 0
1150+
}
1151+
1152+
// GetRPMStrategy 获取 RPM 策略
1153+
// "tiered" = 三区模型(默认), "sticky_exempt" = 粘性豁免
1154+
func (a *Account) GetRPMStrategy() string {
1155+
if a.Extra == nil {
1156+
return "tiered"
1157+
}
1158+
if v, ok := a.Extra["rpm_strategy"]; ok {
1159+
if s, ok := v.(string); ok && s == "sticky_exempt" {
1160+
return "sticky_exempt"
1161+
}
1162+
}
1163+
return "tiered"
1164+
}
1165+
1166+
// GetRPMStickyBuffer 获取 RPM 粘性缓冲数量
1167+
// tiered 模式下的黄区大小,默认为 base_rpm 的 20%(至少 1)
1168+
func (a *Account) GetRPMStickyBuffer() int {
1169+
if a.Extra == nil {
1170+
return 0
1171+
}
1172+
if v, ok := a.Extra["rpm_sticky_buffer"]; ok {
1173+
val := parseExtraInt(v)
1174+
if val > 0 {
1175+
return val
1176+
}
1177+
}
1178+
base := a.GetBaseRPM()
1179+
buffer := base / 5
1180+
if buffer < 1 && base > 0 {
1181+
buffer = 1
1182+
}
1183+
return buffer
1184+
}
1185+
1186+
// CheckRPMSchedulability 根据当前 RPM 计数检查调度状态
1187+
// 复用 WindowCostSchedulability 三态:Schedulable / StickyOnly / NotSchedulable
1188+
func (a *Account) CheckRPMSchedulability(currentRPM int) WindowCostSchedulability {
1189+
baseRPM := a.GetBaseRPM()
1190+
if baseRPM <= 0 {
1191+
return WindowCostSchedulable
1192+
}
1193+
1194+
if currentRPM < baseRPM {
1195+
return WindowCostSchedulable
1196+
}
1197+
1198+
strategy := a.GetRPMStrategy()
1199+
if strategy == "sticky_exempt" {
1200+
return WindowCostStickyOnly // 粘性豁免无红区
1201+
}
1202+
1203+
// tiered: 黄区 + 红区
1204+
buffer := a.GetRPMStickyBuffer()
1205+
if currentRPM < baseRPM+buffer {
1206+
return WindowCostStickyOnly
1207+
}
1208+
return WindowCostNotSchedulable
1209+
}
1210+
11401211
// CheckWindowCostSchedulability 根据当前窗口费用检查调度状态
11411212
// - 费用 < 阈值: WindowCostSchedulable(可正常调度)
11421213
// - 费用 >= 阈值 且 < 阈值+预留: WindowCostStickyOnly(仅粘性会话)
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package service
2+
3+
import "testing"
4+
5+
func TestGetBaseRPM(t *testing.T) {
6+
tests := []struct {
7+
name string
8+
extra map[string]any
9+
expected int
10+
}{
11+
{"nil extra", nil, 0},
12+
{"no key", map[string]any{}, 0},
13+
{"zero", map[string]any{"base_rpm": 0}, 0},
14+
{"int value", map[string]any{"base_rpm": 15}, 15},
15+
{"float value", map[string]any{"base_rpm": 15.0}, 15},
16+
}
17+
for _, tt := range tests {
18+
t.Run(tt.name, func(t *testing.T) {
19+
a := &Account{Extra: tt.extra}
20+
if got := a.GetBaseRPM(); got != tt.expected {
21+
t.Errorf("GetBaseRPM() = %d, want %d", got, tt.expected)
22+
}
23+
})
24+
}
25+
}
26+
27+
func TestGetRPMStrategy(t *testing.T) {
28+
tests := []struct {
29+
name string
30+
extra map[string]any
31+
expected string
32+
}{
33+
{"nil extra", nil, "tiered"},
34+
{"no key", map[string]any{}, "tiered"},
35+
{"tiered", map[string]any{"rpm_strategy": "tiered"}, "tiered"},
36+
{"sticky_exempt", map[string]any{"rpm_strategy": "sticky_exempt"}, "sticky_exempt"},
37+
{"invalid", map[string]any{"rpm_strategy": "foobar"}, "tiered"},
38+
}
39+
for _, tt := range tests {
40+
t.Run(tt.name, func(t *testing.T) {
41+
a := &Account{Extra: tt.extra}
42+
if got := a.GetRPMStrategy(); got != tt.expected {
43+
t.Errorf("GetRPMStrategy() = %q, want %q", got, tt.expected)
44+
}
45+
})
46+
}
47+
}
48+
49+
func TestCheckRPMSchedulability(t *testing.T) {
50+
tests := []struct {
51+
name string
52+
extra map[string]any
53+
currentRPM int
54+
expected WindowCostSchedulability
55+
}{
56+
{"disabled", map[string]any{}, 100, WindowCostSchedulable},
57+
{"green zone", map[string]any{"base_rpm": 15}, 10, WindowCostSchedulable},
58+
{"yellow zone tiered", map[string]any{"base_rpm": 15}, 15, WindowCostStickyOnly},
59+
{"red zone tiered", map[string]any{"base_rpm": 15}, 18, WindowCostNotSchedulable},
60+
{"sticky_exempt at limit", map[string]any{"base_rpm": 15, "rpm_strategy": "sticky_exempt"}, 15, WindowCostStickyOnly},
61+
{"sticky_exempt over limit", map[string]any{"base_rpm": 15, "rpm_strategy": "sticky_exempt"}, 100, WindowCostStickyOnly},
62+
{"custom buffer", map[string]any{"base_rpm": 10, "rpm_sticky_buffer": 5}, 14, WindowCostStickyOnly},
63+
{"custom buffer red", map[string]any{"base_rpm": 10, "rpm_sticky_buffer": 5}, 15, WindowCostNotSchedulable},
64+
}
65+
for _, tt := range tests {
66+
t.Run(tt.name, func(t *testing.T) {
67+
a := &Account{Extra: tt.extra}
68+
if got := a.CheckRPMSchedulability(tt.currentRPM); got != tt.expected {
69+
t.Errorf("CheckRPMSchedulability(%d) = %d, want %d", tt.currentRPM, got, tt.expected)
70+
}
71+
})
72+
}
73+
}

0 commit comments

Comments
 (0)