thread-master/apps/backend/internal/module/radar/usecase/sweep_schedule.go

103 lines
3.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package usecase
import (
"context"
"fmt"
"time"
"apps/backend/internal/module/radar/domain"
)
// DailySweepHourUTC is the fixed daily schedule (spec §4.2UTC 22:00 =台北 06:00)。
const DailySweepHourUTC = 22
// SweepJobScheduler schedules one radar_sweep job per watch per UTC day.
// Implemented by a thin adapter over job usecase.Service (see worker / service_context).
type SweepJobScheduler interface {
ScheduleRadarSweep(ctx context.Context, ownerUID int64, watchID string, runAt int64) (jobID string, err error)
}
// SweepJobSchedulerFunc adapts a function to SweepJobScheduler.
type SweepJobSchedulerFunc func(ctx context.Context, ownerUID int64, watchID string, runAt int64) (jobID string, err error)
func (f SweepJobSchedulerFunc) ScheduleRadarSweep(ctx context.Context, ownerUID int64, watchID string, runAt int64) (string, error) {
return f(ctx, ownerUID, watchID, runAt)
}
// ScheduleDailySweeps 在已過當日 UTC 22:00 時,為每個 active watch 建一筆 radar_sweep Job。
//
// 呼叫端必須已持有 worker maintenance Redis lockvalue = workerID
// 本函式本身不做分散式鎖;未過 22:00 時回 0 且不建 Job。
//
// 回傳建立(或已存在而回傳)的 job 數。
func (s *Service) ScheduleDailySweeps(ctx context.Context, now time.Time) (int, error) {
if s.SweepJobs == nil {
return 0, fmt.Errorf("%w: sweep job scheduler not configured", domain.ErrNotReady)
}
if now.IsZero() {
now = time.Now().UTC()
} else {
now = now.UTC()
}
if !PastDailySweepSlot(now) {
return 0, nil
}
runAt := DailySweepRunAt(now)
watches, err := s.Repo.ListAllActiveWatches(ctx)
if err != nil {
return 0, err
}
n := 0
for _, w := range watches {
if w == nil || w.Status != domain.WatchActive {
continue
}
if _, err := s.SweepJobs.ScheduleRadarSweep(ctx, w.OwnerUID, w.ID, runAt); err != nil {
return n, fmt.Errorf("schedule watch %s owner %d: %w", w.ID, w.OwnerUID, err)
}
n++
}
return n, nil
}
// PastDailySweepSlot reports whether now is at or after today's UTC 22:00.
func PastDailySweepSlot(now time.Time) bool {
now = now.UTC()
slot := time.Date(now.Year(), now.Month(), now.Day(), DailySweepHourUTC, 0, 0, 0, time.UTC)
return !now.Before(slot)
}
// DailySweepRunAt returns unix-ns for today's UTC 22:00 (the slot being scheduled).
func DailySweepRunAt(now time.Time) int64 {
now = now.UTC()
slot := time.Date(now.Year(), now.Month(), now.Day(), DailySweepHourUTC, 0, 0, 0, time.UTC)
return slot.UnixNano()
}
// BeginSweepRecord creates the RadarSweep shell for a claimed job (T527).
// Fetch / judge (T528T530) attach progress onto the same record via UpdateSweep.
func (s *Service) BeginSweepRecord(ctx context.Context, ownerUID int64, watchID, jobID, path string) (*domain.RadarSweep, error) {
if ownerUID <= 0 || watchID == "" {
return nil, fmt.Errorf("%w: owner_uid and watch_id required", domain.ErrValidation)
}
if path == "" {
path = domain.SweepPathAPI
}
sw := &domain.RadarSweep{
ID: domain.NewID(),
OwnerUID: ownerUID,
WatchID: watchID,
JobID: jobID,
Path: path,
StartedAt: domain.NowNano(),
}
if err := sw.Normalize(); err != nil {
return nil, err
}
if err := s.Repo.CreateSweep(ctx, sw); err != nil {
return nil, err
}
return sw, nil
}