Files
wehub-resource-sync f99010fae1
CI / lint (push) Failing after 1s
CI / frontend (push) Failing after 1s
CI / scripts (push) Failing after 1s
CI / Go Test (ubuntu-latest) (push) Failing after 0s
CI / frontend-node-25 (push) Failing after 1s
CI / docs (push) Failing after 0s
CI / coverage (push) Failing after 0s
CI / e2e (push) Failing after 0s
Docker / build-and-push (push) Failing after 1s
CI / integration (push) Failing after 4m43s
CI / Go Test (windows-latest) (push) Has been cancelled
CI / Desktop Unit Tests (Windows) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux (arm64)) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Windows) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (aarch64)) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (x86_64)) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:30:36 +08:00

169 lines
3.0 KiB
Go

package server
import (
"context"
"net/http"
"sync"
"time"
)
// IdleTracker tracks external client activity and internal write work for a
// daemon that should self-reap after a quiet period.
type IdleTracker struct {
mu sync.Mutex
timeout time.Duration
onIdle func()
lastExternal time.Time
activeExternal int
activeWork int
draining bool
notify chan struct{}
}
func NewIdleTracker(timeout time.Duration, onIdle func()) *IdleTracker {
return &IdleTracker{
timeout: timeout,
onIdle: onIdle,
lastExternal: time.Now(),
notify: make(chan struct{}, 1),
}
}
func (t *IdleTracker) Wrap(next http.Handler) http.Handler {
if t == nil {
return next
}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !t.beginExternal() {
http.Error(w, "server is shutting down", http.StatusServiceUnavailable)
return
}
defer t.endExternal()
next.ServeHTTP(w, r)
})
}
// BeginWork is nil-safe so foreground serve mode can share the same call sites
// as background daemons without enabling idle reaping.
func (t *IdleTracker) BeginWork() (func(), bool) {
if t == nil {
return func() {}, true
}
t.mu.Lock()
if t.draining {
t.mu.Unlock()
return func() {}, false
}
t.activeWork++
t.mu.Unlock()
t.signal()
var once sync.Once
return func() {
once.Do(func() {
t.mu.Lock()
if t.activeWork > 0 {
t.activeWork--
}
t.mu.Unlock()
t.signal()
})
}, true
}
// Do runs fn as tracked work, skipping it entirely when the daemon is already
// draining. It is nil-safe so foreground serve mode can share the same call
// sites without enabling idle reaping.
func (t *IdleTracker) Do(fn func()) {
done, ok := t.BeginWork()
if !ok {
return
}
defer done()
fn()
}
func (t *IdleTracker) Touch() {
if t == nil {
return
}
t.mu.Lock()
if !t.draining {
t.lastExternal = time.Now()
}
t.mu.Unlock()
t.signal()
}
func (t *IdleTracker) Run(ctx context.Context) {
if t == nil || t.timeout <= 0 {
return
}
for {
wait, fired := t.nextWait()
if fired {
return
}
timer := time.NewTimer(wait)
select {
case <-ctx.Done():
timer.Stop()
return
case <-t.notify:
if !timer.Stop() {
select {
case <-timer.C:
default:
}
}
case <-timer.C:
}
}
}
func (t *IdleTracker) beginExternal() bool {
t.mu.Lock()
defer t.mu.Unlock()
if t.draining {
return false
}
t.lastExternal = time.Now()
t.activeExternal++
return true
}
func (t *IdleTracker) endExternal() {
t.mu.Lock()
if t.activeExternal > 0 {
t.activeExternal--
}
t.lastExternal = time.Now()
t.mu.Unlock()
t.signal()
}
func (t *IdleTracker) nextWait() (time.Duration, bool) {
t.mu.Lock()
defer t.mu.Unlock()
if t.draining {
return 0, true
}
if t.activeExternal > 0 || t.activeWork > 0 {
return t.timeout, false
}
remaining := t.timeout - time.Since(t.lastExternal)
if remaining > 0 {
return remaining, false
}
t.draining = true
go t.onIdle()
return 0, true
}
func (t *IdleTracker) signal() {
select {
case t.notify <- struct{}{}:
default:
}
}