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

118 lines
3.2 KiB
Go

package main
import (
"context"
"log"
"time"
)
// pushReason labels why a push was triggered, for logging.
type pushReason string
const (
reasonStartup pushReason = "startup"
reasonChange pushReason = "change"
reasonInterval pushReason = "interval"
reasonShutdown pushReason = "shutdown"
)
// defaultFlushTimeout bounds the best-effort push performed when the
// loop shuts down, so a stalled PostgreSQL connection cannot block
// process exit indefinitely.
const defaultFlushTimeout = 30 * time.Second
// pushLoop coalesces file-change notifications and a periodic floor
// tick into serialized pushes. A single goroutine (Run) performs all
// pushes, so a push is never concurrent with another push.
//
// The after/floor fields are injectable so the loop is deterministic
// under test. In production, after is time.After and floor is a
// time.Ticker channel.
type pushLoop struct {
debounce time.Duration
dirty chan struct{}
floor <-chan time.Time
after func(time.Duration) <-chan time.Time
push func(ctx context.Context, reason pushReason) error
label string
// flushTimeout bounds the final shutdown-flush push. Zero means
// no bound (used in tests that inject a fake pusher).
flushTimeout time.Duration
}
// newPushLoop builds a production loop with a real debounce timer and
// floor ticker. The caller must Stop the returned ticker.
func newPushLoop(
debounce, interval time.Duration,
push func(context.Context, pushReason) error,
) (*pushLoop, *time.Ticker) {
return newPushLoopWithLabel("pg watch", debounce, interval, push)
}
func newPushLoopWithLabel(
label string,
debounce, interval time.Duration,
push func(context.Context, pushReason) error,
) (*pushLoop, *time.Ticker) {
ticker := time.NewTicker(interval)
return &pushLoop{
debounce: debounce,
dirty: make(chan struct{}, 1),
floor: ticker.C,
after: time.After,
push: push,
label: label,
flushTimeout: defaultFlushTimeout,
}, ticker
}
// NotifyDirty signals that local data changed. Non-blocking: a burst
// collapses into a single pending push.
func (l *pushLoop) NotifyDirty() {
select {
case l.dirty <- struct{}{}:
default:
}
}
// Run blocks until ctx is cancelled, then performs a final flush push.
func (l *pushLoop) Run(ctx context.Context) {
var armed bool
var fire <-chan time.Time
for {
select {
case <-ctx.Done():
// Final best-effort flush with a fresh context so the
// push is not immediately cancelled.
flushCtx := context.Background()
if l.flushTimeout > 0 {
var cancel context.CancelFunc
flushCtx, cancel = context.WithTimeout(flushCtx, l.flushTimeout)
defer cancel()
}
l.doPush(flushCtx, reasonShutdown)
return
case <-l.dirty:
if !armed {
armed = true
fire = l.after(l.debounce)
}
case <-fire:
armed = false
fire = nil
l.doPush(ctx, reasonChange)
case <-l.floor:
// A floor tick supersedes any pending debounce.
armed = false
fire = nil
l.doPush(ctx, reasonInterval)
}
}
}
func (l *pushLoop) doPush(ctx context.Context, reason pushReason) {
if err := l.push(ctx, reason); err != nil {
log.Printf("%s: push (%s) failed: %v", l.label, reason, err)
}
}