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
169 lines
3.0 KiB
Go
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:
|
|
}
|
|
}
|