package store import ( "context" "time" ) type Task struct { ID int64 `json:"id"` Name string `json:"name"` SrcEndpointID int64 `json:"src_endpoint_id"` DstEndpointID int64 `json:"dst_endpoint_id"` Status string `json:"status"` FolderMapping map[string]string `json:"folder_mapping"` ScheduleIntervalSeconds int64 `json:"schedule_interval_seconds"` ScheduleAnchor *time.Time `json:"-"` Broken bool `json:"broken"` } func (s *Store) CreateTask(ctx context.Context, t Task) (int64, error) { if t.FolderMapping == nil { t.FolderMapping = map[string]string{} } var id int64 err := s.Pool.QueryRow(ctx, `INSERT INTO tasks (name, src_endpoint_id, dst_endpoint_id, folder_mapping) VALUES ($1,$2,$3,$4) RETURNING id`, t.Name, t.SrcEndpointID, t.DstEndpointID, t.FolderMapping).Scan(&id) return id, err } func (s *Store) GetTask(ctx context.Context, id int64) (Task, error) { var t Task err := s.Pool.QueryRow(ctx, `SELECT id, name, src_endpoint_id, dst_endpoint_id, status, folder_mapping, schedule_interval_seconds, schedule_anchor, broken FROM tasks WHERE id=$1`, id). Scan(&t.ID, &t.Name, &t.SrcEndpointID, &t.DstEndpointID, &t.Status, &t.FolderMapping, &t.ScheduleIntervalSeconds, &t.ScheduleAnchor, &t.Broken) return t, err } // DeleteTask removes a task and its accounts/runs/migrated_messages via ON DELETE CASCADE. func (s *Store) DeleteTask(ctx context.Context, id int64) error { _, err := s.Pool.Exec(ctx, `DELETE FROM tasks WHERE id=$1`, id) return err } func (s *Store) ListTasks(ctx context.Context) ([]Task, error) { rows, err := s.Pool.Query(ctx, `SELECT id, name, src_endpoint_id, dst_endpoint_id, status, folder_mapping, schedule_interval_seconds, schedule_anchor, broken FROM tasks ORDER BY id DESC`) if err != nil { return nil, err } defer rows.Close() out := []Task{} for rows.Next() { var t Task if err := rows.Scan(&t.ID, &t.Name, &t.SrcEndpointID, &t.DstEndpointID, &t.Status, &t.FolderMapping, &t.ScheduleIntervalSeconds, &t.ScheduleAnchor, &t.Broken); err != nil { return nil, err } out = append(out, t) } return out, rows.Err() } func (s *Store) SetTaskFolderMapping(ctx context.Context, id int64, mapping map[string]string) error { if mapping == nil { mapping = map[string]string{} } _, err := s.Pool.Exec(ctx, `UPDATE tasks SET folder_mapping=$2 WHERE id=$1`, id, mapping) return err } func (s *Store) SetTaskStatus(ctx context.Context, id int64, status string) error { _, err := s.Pool.Exec(ctx, `UPDATE tasks SET status=$2 WHERE id=$1`, id, status) return err } // TryMarkTaskRunning atomically sets status='running' only if the task is not already running. // Returns true if this call acquired the run (status was not 'running' before), false otherwise. func (s *Store) TryMarkTaskRunning(ctx context.Context, id int64) (bool, error) { ct, err := s.Pool.Exec(ctx, `UPDATE tasks SET status='running' WHERE id=$1 AND status<>'running'`, id) if err != nil { return false, err } return ct.RowsAffected() == 1, nil } // SetTaskSchedule sets the recurrence interval (0 = off), stamps the anchor when // enabling (the first run is one interval after this), and clears broken — any // explicit schedule change un-breaks the task. func (s *Store) SetTaskSchedule(ctx context.Context, id, intervalSeconds int64) error { _, err := s.Pool.Exec(ctx, `UPDATE tasks SET schedule_interval_seconds=$2, schedule_anchor = CASE WHEN $2 > 0 THEN now() ELSE schedule_anchor END, broken = false WHERE id=$1`, id, intervalSeconds) return err } // SetTaskBroken trips the schedule breaker: disables the schedule and flags the // task for operator attention. func (s *Store) SetTaskBroken(ctx context.Context, id int64) error { _, err := s.Pool.Exec(ctx, `UPDATE tasks SET broken=true, schedule_interval_seconds=0 WHERE id=$1`, id) return err } // SchedulableTask is the per-tick decision input for the scheduler. type SchedulableTask struct { ID int64 IntervalSeconds int64 Anchor time.Time LastFinished *time.Time } // ListSchedulableTasks returns tasks eligible to auto-run: schedule on, not // broken, not currently running — each joined with its last finished run time. func (s *Store) ListSchedulableTasks(ctx context.Context) ([]SchedulableTask, error) { rows, err := s.Pool.Query(ctx, `SELECT t.id, t.schedule_interval_seconds, t.schedule_anchor, (SELECT max(finished_at) FROM runs r WHERE r.task_id=t.id AND r.finished_at IS NOT NULL) FROM tasks t WHERE t.schedule_interval_seconds > 0 AND NOT t.broken AND t.status <> 'running'`) if err != nil { return nil, err } defer rows.Close() out := []SchedulableTask{} for rows.Next() { var st SchedulableTask if err := rows.Scan(&st.ID, &st.IntervalSeconds, &st.Anchor, &st.LastFinished); err != nil { return nil, err } out = append(out, st) } return out, rows.Err() }