103 lines
3.2 KiB
Go
103 lines
3.2 KiB
Go
|
|
package usecase
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"context"
|
|||
|
|
"fmt"
|
|||
|
|
"time"
|
|||
|
|
|
|||
|
|
"apps/backend/internal/module/radar/domain"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// DailySweepHourUTC is the fixed daily schedule (spec §4.2:UTC 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 lock(value = 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 (T528–T530) 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
|
|||
|
|
}
|