mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-05 13:29:44 +02:00
* 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>
99 lines
2.3 KiB
Go
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
|
|
}
|
|
}
|