Add selectAccounts filter and accountIDs param to orchestrator.Run

This commit is contained in:
2026-07-08 13:37:24 +07:00
parent 692a83a468
commit 315e56046b
4 changed files with 50 additions and 3 deletions
+1 -1
View File
@@ -69,7 +69,7 @@ func (s *Server) handleRun(w http.ResponseWriter, r *http.Request) {
http.Error(w, "bad id", http.StatusBadRequest) http.Error(w, "bad id", http.StatusBadRequest)
return return
} }
runID, err := s.orch.Run(r.Context(), taskID, "manual") runID, err := s.orch.Run(r.Context(), taskID, "manual", nil)
if errors.Is(err, orchestrator.ErrNotTested) { if errors.Is(err, orchestrator.ErrNotTested) {
http.Error(w, "accounts must pass connection tests first", http.StatusConflict) http.Error(w, "accounts must pass connection tests first", http.StatusConflict)
return return
+28 -1
View File
@@ -16,6 +16,7 @@ import (
var ErrNotTested = errors.New("accounts not fully tested") var ErrNotTested = errors.New("accounts not fully tested")
var ErrAlreadyRunning = errors.New("task already running") var ErrAlreadyRunning = errors.New("task already running")
var ErrNoAccountsSelected = errors.New("no matching accounts selected")
// maxAccountErrors caps how many individual error rows one account records per // maxAccountErrors caps how many individual error rows one account records per
// run, so a corrupt mailbox producing thousands of failures can't bloat the // run, so a corrupt mailbox producing thousands of failures can't bloat the
@@ -99,6 +100,26 @@ func (o *Orchestrator) unregisterCancel(accountID int64) {
o.mu.Unlock() o.mu.Unlock()
} }
// selectAccounts narrows accs to those whose ID is in ids, preserving input
// order. An empty or nil ids means "all accounts" — the scheduler and the
// unfiltered manual run rely on this.
func selectAccounts(accs []store.Account, ids []int64) []store.Account {
if len(ids) == 0 {
return accs
}
want := make(map[int64]struct{}, len(ids))
for _, id := range ids {
want[id] = struct{}{}
}
out := make([]store.Account, 0, len(ids))
for _, a := range accs {
if _, ok := want[a.ID]; ok {
out = append(out, a)
}
}
return out
}
func gateOK(accs []store.Account) bool { func gateOK(accs []store.Account) bool {
if len(accs) == 0 { if len(accs) == 0 {
return false return false
@@ -173,7 +194,7 @@ func shouldBreak(trigger string, totErr int64) bool {
return trigger == "scheduled" && totErr > 0 return trigger == "scheduled" && totErr > 0
} }
func (o *Orchestrator) Run(ctx context.Context, taskID int64, trigger string) (int64, error) { func (o *Orchestrator) Run(ctx context.Context, taskID int64, trigger string, accountIDs []int64) (int64, error) {
task, err := o.store.GetTask(ctx, taskID) task, err := o.store.GetTask(ctx, taskID)
if err != nil { if err != nil {
return 0, err return 0, err
@@ -182,6 +203,12 @@ func (o *Orchestrator) Run(ctx context.Context, taskID int64, trigger string) (i
if err != nil { if err != nil {
return 0, err return 0, err
} }
accs = selectAccounts(accs, accountIDs)
// A non-empty request that matched nothing is a client error, distinct
// from "not tested".
if len(accountIDs) > 0 && len(accs) == 0 {
return 0, ErrNoAccountsSelected
}
if !gateOK(accs) { if !gateOK(accs) {
return 0, ErrNotTested return 0, ErrNotTested
} }
@@ -22,3 +22,23 @@ func TestGateOK(t *testing.T) {
t.Fatal("empty accounts must fail gate") t.Fatal("empty accounts must fail gate")
} }
} }
func TestSelectAccounts(t *testing.T) {
accs := []store.Account{{ID: 1}, {ID: 2}, {ID: 3}}
if got := selectAccounts(accs, nil); len(got) != 3 {
t.Fatalf("nil ids must return all, got %d", len(got))
}
if got := selectAccounts(accs, []int64{}); len(got) != 3 {
t.Fatalf("empty ids must return all, got %d", len(got))
}
got := selectAccounts(accs, []int64{3, 1})
if len(got) != 2 || got[0].ID != 1 || got[1].ID != 3 {
t.Fatalf("must keep matching ids in input order, got %+v", got)
}
if got := selectAccounts(accs, []int64{99}); len(got) != 0 {
t.Fatalf("unknown ids must yield empty, got %d", len(got))
}
}
+1 -1
View File
@@ -67,7 +67,7 @@ func (s *Scheduler) tick(ctx context.Context) {
return return
} }
for _, id := range dueTaskIDs(tasks, time.Now()) { for _, id := range dueTaskIDs(tasks, time.Now()) {
if _, err := s.orch.Run(ctx, id, "scheduled"); err != nil { if _, err := s.orch.Run(ctx, id, "scheduled", nil); err != nil {
// ErrAlreadyRunning / ErrNotTested are expected races/edge cases, not fatal. // ErrAlreadyRunning / ErrNotTested are expected races/edge cases, not fatal.
slog.Info("scheduler: run skipped", "task", id, "err", err) slog.Info("scheduler: run skipped", "task", id, "err", err)
} }