package usecase import ( "context" "errors" "haixun-backend/internal/library/clock" "haixun-backend/internal/model/job/domain/entity" "haixun-backend/internal/model/job/domain/enum" domrepo "haixun-backend/internal/model/job/domain/repository" domusecase "haixun-backend/internal/model/job/domain/usecase" "sync" "go.mongodb.org/mongo-driver/bson/primitive" ) type memoryTemplateRepo struct { template *entity.Template } func (m *memoryTemplateRepo) EnsureIndexes(context.Context) error { return nil } func (m *memoryTemplateRepo) List(context.Context) ([]*entity.Template, error) { if m.template == nil { return nil, nil } return []*entity.Template{m.template}, nil } func (m *memoryTemplateRepo) FindByType(context.Context, string) (*entity.Template, error) { return m.template, nil } func (m *memoryTemplateRepo) Upsert(_ context.Context, template *entity.Template) (*entity.Template, error) { m.template = template return template, nil } type memoryRunRepo struct { mu sync.Mutex run *entity.Run succeeded map[string]*entity.Run } func newMemoryRunRepo(run *entity.Run) *memoryRunRepo { return &memoryRunRepo{run: run, succeeded: map[string]*entity.Run{}} } func (m *memoryRunRepo) EnsureIndexes(context.Context) error { return nil } func (m *memoryRunRepo) Create(ctx context.Context, run *entity.Run) (*entity.Run, error) { m.mu.Lock() defer m.mu.Unlock() if run.ID.IsZero() { run.ID = primitive.NewObjectID() } now := clock.NowUnixNano() run.CreateAt = now run.UpdateAt = now m.run = run return run, nil } func (m *memoryRunRepo) Update(ctx context.Context, run *entity.Run) (*entity.Run, error) { m.mu.Lock() defer m.mu.Unlock() run.UpdateAt = clock.NowUnixNano() m.run = run if run.Status == enum.RunStatusSucceeded && run.DedupeKey != "" { m.succeeded[run.TemplateType+":"+run.DedupeKey] = run } return run, nil } func (m *memoryRunRepo) UpdateIfStatus(ctx context.Context, run *entity.Run, allowed []enum.RunStatus) (*entity.Run, error) { m.mu.Lock() defer m.mu.Unlock() if m.run != nil { ok := false for _, status := range allowed { if m.run.Status == status { ok = true break } } if !ok { return nil, errors.New("job state changed; update rejected") } } run.UpdateAt = clock.NowUnixNano() m.run = run if run.Status == enum.RunStatusSucceeded && run.DedupeKey != "" { m.succeeded[run.TemplateType+":"+run.DedupeKey] = run } return run, nil } func (m *memoryRunRepo) UpdateIfLocked(ctx context.Context, run *entity.Run, workerID string, allowed []enum.RunStatus) (*entity.Run, error) { m.mu.Lock() defer m.mu.Unlock() if m.run != nil { if m.run.LockedBy != "" && m.run.LockedBy != workerID { return nil, errors.New("job lock held by another worker") } ok := false for _, status := range allowed { if m.run.Status == status { ok = true break } } if !ok { return nil, errors.New("job state changed; update rejected") } } run.UpdateAt = clock.NowUnixNano() m.run = run if run.Status == enum.RunStatusSucceeded && run.DedupeKey != "" { m.succeeded[run.TemplateType+":"+run.DedupeKey] = run } return run, nil } func (m *memoryRunRepo) FindByID(context.Context, string) (*entity.Run, error) { m.mu.Lock() defer m.mu.Unlock() return cloneRun(m.run), nil } func (m *memoryRunRepo) List(context.Context, domrepo.RunListFilter, int64, int64) ([]*entity.Run, int64, error) { return nil, 0, nil } func (m *memoryRunRepo) FindActiveByScope(context.Context, string, string, string) ([]*entity.Run, error) { m.mu.Lock() defer m.mu.Unlock() if m.run == nil { return nil, nil } switch m.run.Status { case enum.RunStatusPending, enum.RunStatusQueued, enum.RunStatusRunning, enum.RunStatusWaitingWorker, enum.RunStatusCancelRequested: return []*entity.Run{m.run}, nil default: return nil, nil } } func (m *memoryRunRepo) FindSucceededByDedupeKey(_ context.Context, templateType, dedupeKey string) (*entity.Run, error) { m.mu.Lock() defer m.mu.Unlock() return m.succeeded[templateType+":"+dedupeKey], nil } func (m *memoryRunRepo) FindPendingDue(_ context.Context, now int64, _ int64) ([]*entity.Run, error) { m.mu.Lock() defer m.mu.Unlock() if m.run == nil || m.run.Status != enum.RunStatusPending || m.run.ScheduledAt == nil { return nil, nil } if *m.run.ScheduledAt <= now { return []*entity.Run{m.run}, nil } return nil, nil } func (m *memoryRunRepo) FindCancelRequestedBefore(_ context.Context, before int64, _ int64) ([]*entity.Run, error) { m.mu.Lock() defer m.mu.Unlock() if m.run == nil || m.run.Status != enum.RunStatusCancelRequested || m.run.CancelRequestedAt == nil { return nil, nil } if *m.run.CancelRequestedAt <= before { return []*entity.Run{m.run}, nil } return nil, nil } func (m *memoryRunRepo) FindRunningTimedOut(_ context.Context, now int64, _ int64) ([]*entity.Run, error) { m.mu.Lock() defer m.mu.Unlock() if m.run == nil || m.run.LockedUntil == nil { return nil, nil } if m.run.Status != enum.RunStatusRunning && m.run.Status != enum.RunStatusWaitingWorker { return nil, nil } if *m.run.LockedUntil <= now { return []*entity.Run{m.run}, nil } return nil, nil } type memoryScheduleRepo struct { mu sync.Mutex schedules []*entity.Schedule } func (m *memoryScheduleRepo) EnsureIndexes(context.Context) error { return nil } func (m *memoryScheduleRepo) Create(_ context.Context, schedule *entity.Schedule) (*entity.Schedule, error) { m.mu.Lock() defer m.mu.Unlock() if schedule.ID.IsZero() { schedule.ID = primitive.NewObjectID() } now := clock.NowUnixNano() schedule.CreateAt = now schedule.UpdateAt = now m.schedules = append(m.schedules, schedule) return schedule, nil } func (m *memoryScheduleRepo) Update(_ context.Context, schedule *entity.Schedule) (*entity.Schedule, error) { m.mu.Lock() defer m.mu.Unlock() schedule.UpdateAt = clock.NowUnixNano() for i, item := range m.schedules { if item.ID == schedule.ID { m.schedules[i] = schedule return schedule, nil } } m.schedules = append(m.schedules, schedule) return schedule, nil } func (m *memoryScheduleRepo) FindByID(_ context.Context, id string) (*entity.Schedule, error) { m.mu.Lock() defer m.mu.Unlock() objectID, _ := primitive.ObjectIDFromHex(id) for _, item := range m.schedules { if item.ID == objectID { return item, nil } } return nil, nil } func (m *memoryScheduleRepo) List(_ context.Context, _ domrepo.ScheduleListFilter, _, _ int64) ([]*entity.Schedule, int64, error) { m.mu.Lock() defer m.mu.Unlock() return m.schedules, int64(len(m.schedules)), nil } func (m *memoryScheduleRepo) FindDue(_ context.Context, now int64, _ int64) ([]*entity.Schedule, error) { m.mu.Lock() defer m.mu.Unlock() var due []*entity.Schedule for _, item := range m.schedules { if item.Enabled && item.NextRunAt <= now { due = append(due, item) } } return due, nil } type memoryEventRepo struct { events []*entity.Event } func (m *memoryEventRepo) EnsureIndexes(context.Context) error { return nil } func (m *memoryEventRepo) Append(_ context.Context, event *entity.Event) error { m.events = append(m.events, event) return nil } func (m *memoryEventRepo) ListByJobID(_ context.Context, jobID string, limit int64) ([]*entity.Event, error) { if limit <= 0 { limit = 50 } var out []*entity.Event for _, event := range m.events { if event.JobID == jobID { out = append(out, event) } } if int64(len(out)) > limit { out = out[:limit] } return out, nil } type memoryQueueRepo struct { mu sync.Mutex queues map[string][]string cancel map[string]string locks map[string]string dedupe map[string]string schedulerLock string } func newMemoryQueueRepo() *memoryQueueRepo { return &memoryQueueRepo{ queues: map[string][]string{}, cancel: map[string]string{}, locks: map[string]string{}, dedupe: map[string]string{}, } } func (m *memoryQueueRepo) Enqueue(_ context.Context, workerType, jobID string) error { m.mu.Lock() defer m.mu.Unlock() m.queues[workerType] = append(m.queues[workerType], jobID) return nil } func (m *memoryQueueRepo) Dequeue(context.Context, string, int) (string, error) { return "", nil } func (m *memoryQueueRepo) RemoveJob(_ context.Context, workerType, jobID string) error { m.mu.Lock() defer m.mu.Unlock() items := m.queues[workerType] filtered := make([]string, 0, len(items)) for _, item := range items { if item != jobID { filtered = append(filtered, item) } } m.queues[workerType] = filtered return nil } func (m *memoryQueueRepo) SetCancelSignal(_ context.Context, jobID, reason string) error { m.mu.Lock() defer m.mu.Unlock() m.cancel[jobID] = reason return nil } func (m *memoryQueueRepo) GetCancelSignal(_ context.Context, jobID string) (bool, string, error) { m.mu.Lock() defer m.mu.Unlock() reason, ok := m.cancel[jobID] return ok, reason, nil } func (m *memoryQueueRepo) ClearCancelSignal(_ context.Context, jobID string) error { m.mu.Lock() defer m.mu.Unlock() delete(m.cancel, jobID) return nil } func (m *memoryQueueRepo) TryLock(_ context.Context, jobID, workerID string, _ int) (bool, error) { m.mu.Lock() defer m.mu.Unlock() if current, ok := m.locks[jobID]; ok && current != workerID { return false, nil } m.locks[jobID] = workerID return true, nil } func (m *memoryQueueRepo) RefreshLock(_ context.Context, jobID, workerID string, _ int) error { m.mu.Lock() defer m.mu.Unlock() if current, ok := m.locks[jobID]; ok && current == workerID { return nil } return errors.New("job lock is not held by worker") } func (m *memoryQueueRepo) ReleaseLock(_ context.Context, jobID, workerID string) error { m.mu.Lock() defer m.mu.Unlock() if current, ok := m.locks[jobID]; ok && current == workerID { delete(m.locks, jobID) } return nil } func (m *memoryQueueRepo) TryAcquireDedupe(_ context.Context, templateType, dedupeHash, jobID string, _ int) (bool, error) { m.mu.Lock() defer m.mu.Unlock() key := templateType + ":" + dedupeHash if _, ok := m.dedupe[key]; ok { return false, nil } m.dedupe[key] = jobID return true, nil } func (m *memoryQueueRepo) ReleaseDedupe(_ context.Context, templateType, dedupeHash string) error { m.mu.Lock() defer m.mu.Unlock() delete(m.dedupe, templateType+":"+dedupeHash) return nil } func (m *memoryQueueRepo) TrySchedulerLock(_ context.Context, holder string, _ int) (bool, error) { m.mu.Lock() defer m.mu.Unlock() if m.schedulerLock != "" && m.schedulerLock != holder { return false, nil } m.schedulerLock = holder return true, nil } func (m *memoryQueueRepo) ReleaseSchedulerLock(_ context.Context, holder string) error { m.mu.Lock() defer m.mu.Unlock() if m.schedulerLock == holder { m.schedulerLock = "" } return nil } func (m *memoryQueueRepo) queued(workerType string) []string { m.mu.Lock() defer m.mu.Unlock() return append([]string(nil), m.queues[workerType]...) } func testUseCase(template *entity.Template, run *entity.Run) domusecase.UseCase { return NewUseCase( &memoryTemplateRepo{template: template}, newMemoryRunRepo(run), &memoryScheduleRepo{}, &memoryEventRepo{}, newMemoryQueueRepo(), ) } func testUseCaseFull(template *entity.Template, runs *memoryRunRepo, schedules *memoryScheduleRepo, queue *memoryQueueRepo) domusecase.UseCase { if schedules == nil { schedules = &memoryScheduleRepo{} } if queue == nil { queue = newMemoryQueueRepo() } return NewUseCase( &memoryTemplateRepo{template: template}, runs, schedules, &memoryEventRepo{}, queue, ) } func cloneRun(run *entity.Run) *entity.Run { if run == nil { return nil } out := *run if run.Progress.Steps != nil { out.Progress.Steps = append([]entity.StepProgress(nil), run.Progress.Steps...) } if run.Payload != nil { out.Payload = map[string]any{} for k, v := range run.Payload { out.Payload[k] = v } } if run.Result != nil { out.Result = map[string]any{} for k, v := range run.Result { out.Result[k] = v } } return &out }