package daemon // idle_timeout.go - 会话空闲超时计时器. // // 区别于心跳超时(HeartbeatService 检测网络是否活跃), // IdleTimer 检测"用户是否还在用"--空闲超时关闭会话释放资源. // // 升华改进(ELEVATED): 早期方案 使用硬编码的 24 小时绝对超时-- // 无论操作员是否还在活跃,24 小时后强制关闭.这在仓储场景有问题: // - 夜班操作员持续使用:绝对超时在班中切断连接 // - 操作员开着页面但不操作:浪费资源,不应保留太久 // // 我们改为"空闲超时"(idle timeout):只要有活动就重置计时器, // 真正长时间无操作才关闭.这对所有场景都更合理. // 替代方案:<绝对超时 + 活跃延期> - 否决:过于复杂,两个时钟难以推理. import ( "sync" "time" ) // IdleTimer 是可重置的空闲超时计时器. // // 精妙之处(CLEVER): 不使用 time.AfterFunc + 手动 Stop/Reset 的组合-- // AfterFunc 的 Reset 有竞争条件(Reset 和回调并发触发时行为不确定). // 我们改用独立 goroutine + channel 信号控制: // - reset channel 接收重置信号,goroutine 重新开始计时 // - stop channel 接收停止信号,goroutine 退出 // // 这是 Go 中实现"可重置定时器"的标准模式(避免 time.Timer Reset race). type IdleTimer struct { timeout time.Duration onIdle func() // 超时回调(在独立 goroutine 中调用) resetCh chan struct{} stopCh chan struct{} once sync.Once } // NewIdleTimer 创建空闲计时器并立即启动. // // timeout: 空闲超时时长(无活动多久后触发). // onIdle: 超时后的回调(在后台 goroutine 中执行,不可重入). func NewIdleTimer(timeout time.Duration, onIdle func()) *IdleTimer { t := &IdleTimer{ timeout: timeout, onIdle: onIdle, resetCh: make(chan struct{}, 1), // 缓冲 1:防止 Reset 阻塞 stopCh: make(chan struct{}), } go t.run() return t } // Reset 重置计时器(有活动时调用). // 安全地从任意 goroutine 调用,非阻塞. // // 精妙之处(CLEVER): resetCh 缓冲为 1--如果上一次 Reset 的信号还没被消费, // 新的 Reset 不会再写入(select default 丢弃).这防止了大量并发活动导致 // resetCh 堆积,同时保证"有活动"这一语义被正确传达. // 两次快速 Reset 和一次 Reset 对计时器的效果是相同的. func (t *IdleTimer) Reset() { select { case t.resetCh <- struct{}{}: default: // 已有未处理的 reset 信号,不再重复写入 } } // Stop 永久停止计时器,回调不会再被触发.幂等. func (t *IdleTimer) Stop() { t.once.Do(func() { close(t.stopCh) }) } // run 是计时器的主循环 goroutine. func (t *IdleTimer) run() { timer := time.NewTimer(t.timeout) defer timer.Stop() for { select { case <-t.stopCh: return case <-t.resetCh: // 有活动,重置计时器 // 精妙之处(CLEVER): 必须先 Stop() 再 Reset()-- // 如果计时器已触发(channel 中已有值),直接 Reset 不消费就重置, // 导致立即再次触发.Stop 返回 false 时需要 drain channel. if !timer.Stop() { select { case <-timer.C: default: } } timer.Reset(t.timeout) case <-timer.C: // 超时,触发回调 go t.onIdle() // 在独立 goroutine 中执行,不阻塞计时器循环 return // 超时后计时器自动停止(不继续循环) } } }