a3216's picture
chore: 同步到上游 1.12.0-panel + 凭证同步/独立启动器/保活
6d60378 verified
Raw History Blame Contribute Delete
2.18 kB
// idle.go 聊天 SSE 流中空闲监控:活跃吐数据续命不掐,静默超过阈值才断流(释放租约)。
package upstream
import (
"context"
"io"
"sync"
"time"
)
// idleMonitoringBody 包在聊天 SSE body 外层:
// 每次读到底层数据(n>0)就刷新 lastRead;后台 goroutine 周期检查,
// 静默超过 idle 就 cancel 请求 context,中断阻塞中的 Read。
type idleMonitoringBody struct {
rc io.ReadCloser
mu sync.Mutex
lastRead time.Time
stopOnce sync.Once
stopCh chan struct{}
cancel context.CancelFunc
}
func (b *idleMonitoringBody) Read(p []byte) (int, error) {
n, err := b.rc.Read(p)
if n > 0 {
b.mu.Lock()
b.lastRead = time.Now()
b.mu.Unlock()
}
return n, err
}
// Close 停掉后台 goroutine、取消请求 context、关闭底流,保证无泄漏。
func (b *idleMonitoringBody) Close() error {
b.stopOnce.Do(func() { close(b.stopCh) })
b.cancel()
return b.rc.Close()
}
func (b *idleMonitoringBody) idleFor() time.Duration {
b.mu.Lock()
defer b.mu.Unlock()
return time.Since(b.lastRead)
}
// monitorBody 若 idle<=0 直接返回原底流(禁用空闲监控);
// 否则包上流中空闲监控。计时从返回 body 之后开始——首字节阶段由
// Transport.ResponseHeaderTimeout 管,这里不抢跑。
func monitorBody(rc io.ReadCloser, idle time.Duration, cancel context.CancelFunc) io.ReadCloser {
if idle <= 0 {
return rc
}
b := &idleMonitoringBody{
rc: rc,
lastRead: time.Now(),
stopCh: make(chan struct{}),
cancel: cancel,
}
go func() {
t := time.NewTicker(idleTick(idle))
defer t.Stop()
for {
select {
case <-b.stopCh:
return
case <-t.C:
if b.idleFor() > idle {
cancel()
return
}
}
}
}()
return b
}
// idleTick 返回监控周期:idle/4,钳在 [10ms, 1s]。小 idle 也能快速发现,大小值避免空转。
func idleTick(idle time.Duration) time.Duration {
d := idle / 4
if d > time.Second {
d = time.Second
}
if d < 10*time.Millisecond {
d = 10 * time.Millisecond
}
return d
}