59 lines
1.6 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|