Files
multica/server/internal/daemon/helpers.go
devv-eve 9db91e89f5 feat: add daemon websocket task wakeups (#1772)
* feat: add daemon websocket task wakeups

* feat: fan out daemon wakeups across nodes

* fix: dedupe daemon wakeup loopback events

* fix: lengthen daemon polling fallback interval

---------

Co-authored-by: Eve <eve@multica.ai>
2026-04-28 16:07:24 +08:00

99 lines
2.3 KiB
Go

package daemon
import (
"context"
"fmt"
"os"
"regexp"
"strconv"
"strings"
"time"
)
func envOrDefault(key, fallback string) string {
value := strings.TrimSpace(os.Getenv(key))
if value == "" {
return fallback
}
return value
}
func durationFromEnv(key string, fallback time.Duration) (time.Duration, error) {
value := strings.TrimSpace(os.Getenv(key))
if value == "" {
return fallback, nil
}
d, err := parseFlexDuration(value)
if err != nil {
return 0, fmt.Errorf("%s: invalid duration %q: %w", key, value, err)
}
return d, nil
}
// dayUnit matches a decimal number (with optional leading digits) followed by
// `d` (days), so both "5d" and "1.5d" are captured whole and expanded to hours.
var dayUnit = regexp.MustCompile(`(\d*\.\d+|\d+)d`)
// parseFlexDuration accepts the standard Go time.ParseDuration syntax plus a
// `d` (day) suffix, which the stdlib rejects. "5d" → 120h, "1d12h" → 36h,
// "0.5d" → 12h. Overflow or malformed numbers propagate as errors.
func parseFlexDuration(value string) (time.Duration, error) {
var convErr error
expanded := dayUnit.ReplaceAllStringFunc(value, func(match string) string {
days, err := strconv.ParseFloat(match[:len(match)-1], 64)
if err != nil {
convErr = err
return match
}
// time.ParseDuration handles fractional hours natively, and rejects
// overflow on its own.
return strconv.FormatFloat(days*24, 'f', -1, 64) + "h"
})
if convErr != nil {
return 0, convErr
}
return time.ParseDuration(expanded)
}
func intFromEnv(key string, fallback int) (int, error) {
value := strings.TrimSpace(os.Getenv(key))
if value == "" {
return fallback, nil
}
n, err := strconv.Atoi(value)
if err != nil {
return 0, fmt.Errorf("%s: invalid integer %q: %w", key, value, err)
}
return n, nil
}
func sleepWithContext(ctx context.Context, d time.Duration) error {
timer := time.NewTimer(d)
defer timer.Stop()
select {
case <-ctx.Done():
return ctx.Err()
case <-timer.C:
return nil
}
}
func sleepWithContextOrWakeup(ctx context.Context, d time.Duration, wakeups <-chan struct{}) error {
if wakeups == nil {
return sleepWithContext(ctx, d)
}
timer := time.NewTimer(d)
defer timer.Stop()
select {
case <-ctx.Done():
return ctx.Err()
case <-wakeups:
return nil
case <-timer.C:
return nil
}
}