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
|
||
}
|