144 lines
5.0 KiB
Go
144 lines
5.0 KiB
Go
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()
|
|
}
|