File size: 2,816 Bytes
6d60378
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
// 连败降权(issue #114):NoteFailures 计数 + 达阈临时出池 + NoteSuccess 清零。
// 与冷却/熔断「并存取更长者不叠加」由 entry.healthy 的并列或门天然保证
// (三个截止是或门,任一未到期即不可选,生效的一定是最远者——见 entry.degradeUntil 注释)。
package pool

import "time"

// NoteFailures 记录一次「不罚号的失败」(issue #114 连败兜底的喂入口):
// consecutiveFails++,达到 degradeThreshold 触发降权(degradeUntil = now +
// 降权时长,计数清零供下一轮重新累计——同 recordBreakerFailureLocked 哲学)。
// **已在降权期内**(degradeUntil 未到期)再次达阈:不延长、不翻倍(防用户重试
// 把降权越堆越厚,与 CooldownSoftRate 兜底探测同哲学)——计数清零,到期放行后
// 需重新连败满阈值才再降。成功(NoteSuccess)清零计数并清降权截止。
//
// 谁该喂:ErrClient(未知 4xx,applyErrorPolicy default 只换号不罚)与传输层
// 失败(handler 网络抖动分支,同样只换号不罚)——两者都是「不知道原因的失败」,
// 正是连败兜底的目标形态。带权威分类的错误(429/WAF/402/5xx/12153/11140/11102)
// 各有「知道原因的惩罚」(冷却/熔断/禁用/负缓存),不喂本计数器——惩罚已存在,
// 再喂连败是重复计罚(任务书纪律:两机制并存取更长者,不叠加)。
func (p *Pool) NoteFailures(uid string) {
	p.mu.Lock()
	defer p.mu.Unlock()
	e, ok := p.byUID[uid]
	if !ok {
		return
	}
	e.consecutiveFails++
	if e.consecutiveFails < p.degradeThreshold {
		p.dirty.Store(true)
		return
	}
	e.consecutiveFails = 0
	if !e.degradeUntil.IsZero() && time.Now().Before(e.degradeUntil) {
		// 降权期内的连败达阈:不延长(取更长者语义由 healthy 或门保证)。计数
		// 已清零——到期放行后需重新连败满阈值才再降。
		p.dirty.Store(true)
		return
	}
	e.degradeUntil = time.Now().Add(p.degradeDurationLocked())
	p.dirty.Store(true)
}

// degradeDurationLocked 返回降权时长(当前实现为固定 degradeCooldown,钳到
// [defaultDegradeCooldown, degradeCooldownMax])。不做指数升级:触发计数在达阈时
// 清零、不持久化历史触发次数,档位不可推导;固定时长已满足「临时出池一段时间」
// 的语义(真持续坏号会在下一轮 5 连败里再降一次)。调用方必须已持有 p.mu。
func (p *Pool) degradeDurationLocked() time.Duration {
	d := p.degradeCooldown
	if d <= 0 {
		d = defaultDegradeCooldown
	}
	max := p.degradeCooldownMax
	if max <= 0 {
		max = defaultDegradeCooldownMax
	}
	if d > max {
		d = max
	}
	return d
}