Files
go-sip/internal/daily/midnight.go
T

59 lines
1.6 KiB
Go

// Package daily provides the project's fixed Shanghai midnight calendar timer.
// It is not configurable and does not track permanent sweep history.
package daily
import (
"context"
"errors"
"log/slog"
"time"
_ "time/tzdata"
)
func NextMidnight(now time.Time) (time.Time, error) {
zone, err := time.LoadLocation("Asia/Shanghai")
if err != nil {
return time.Time{}, err
}
local := now.In(zone)
return time.Date(local.Year(), local.Month(), local.Day()+1, 0, 0, 0, 0, zone), nil
}
func Run(ctx context.Context, sweep func(context.Context) error) error {
return run(ctx, sweep, time.Now, func(delay time.Duration) (<-chan time.Time, func()) {
timer := time.NewTimer(delay)
return timer.C, func() { timer.Stop() }
})
}
func run(ctx context.Context, sweep func(context.Context) error, now func() time.Time, newTimer func(time.Duration) (<-chan time.Time, func())) error {
if ctx == nil || sweep == nil || now == nil || newTimer == nil {
return errors.New("midnight cleanup requires context and a sweep")
}
for {
if ctx.Err() != nil {
return nil
}
current := now()
next, err := NextMidnight(current)
if err != nil {
return err
}
slog.InfoContext(ctx, "Midnight cleanup scheduled", "timezone", "Asia/Shanghai", "next_at", next)
tick, stop := newTimer(next.Sub(current))
select {
case <-ctx.Done():
stop()
return nil
case <-tick:
stop()
}
if now().Before(next) {
continue
} // a wall-clock rollback must not run early
if err := sweep(ctx); err != nil && ctx.Err() == nil {
slog.ErrorContext(ctx, "Midnight cleanup incomplete; retry scheduled for next midnight", "error", err)
}
}
}