Skip to content

Commit

Permalink
Merge pull request #526 from sbondCo/better-tasks
Browse files Browse the repository at this point in the history
Better tasks
  • Loading branch information
IRHM authored May 21, 2024
2 parents ef12ad5 + 127b248 commit 62ca498
Show file tree
Hide file tree
Showing 12 changed files with 365 additions and 18 deletions.
3 changes: 3 additions & 0 deletions server/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ type ServerConfig struct {
RADARR []RadarrSettings `json:",omitempty"`
TWITCH game.IGDB `json:",omitempty"`

// Optional: Schedule for tasks.
TASK_SCHEDULE map[string]int `json:",omitempty"`

// Enable/disable debug logging. Useful for when trying
// to figure out exactly what the server is doing at a point
// of failure.
Expand Down
5 changes: 5 additions & 0 deletions server/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,17 @@ require (
github.com/cloudwego/iasm v0.2.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-co-op/gocron/v2 v2.5.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.20.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/gomodule/redigo v1.8.9 // indirect
github.com/google/go-cmp v0.5.9 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/jonboulle/clockwork v0.4.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/kr/text v0.2.0 // indirect
Expand All @@ -41,10 +44,12 @@ require (
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.1 // indirect
github.com/robfig/cron/v3 v3.0.1 // indirect
github.com/robfig/go-cache v0.0.0-20130306151617-9fc39e0dbf62 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
golang.org/x/arch v0.7.0 // indirect
golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f // indirect
golang.org/x/net v0.24.0 // indirect
golang.org/x/sys v0.20.0 // indirect
golang.org/x/text v0.15.0 // indirect
Expand Down
10 changes: 10 additions & 0 deletions server/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
github.com/go-co-op/gocron/v2 v2.5.0 h1:ff/TJX9GdTJBDL1il9cyd/Sj3WnS+BB7ZzwHKSNL5p8=
github.com/go-co-op/gocron/v2 v2.5.0/go.mod h1:ckPQw96ZuZLRUGu88vVpd9a6d9HakI14KWahFZtGvNw=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
Expand All @@ -41,12 +43,16 @@ github.com/gomodule/redigo v1.8.9/go.mod h1:7ArFNvsTjH8GMMzB4uy1snslv2BwmginuMs0
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/jonboulle/clockwork v0.4.0 h1:p4Cf1aMWXnXAUh8lVfewRBx1zaTSYKrKMF2g3ST4RZ4=
github.com/jonboulle/clockwork v0.4.0/go.mod h1:xgRqUGwRcjKCO1vbZUEtSLrqKoPSsUpK7fnezOII0kc=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
Expand Down Expand Up @@ -74,6 +80,8 @@ github.com/pelletier/go-toml/v2 v2.2.1 h1:9TA9+T8+8CUCO2+WYnDLCgrYi9+omqKXyjDtos
github.com/pelletier/go-toml/v2 v2.2.1/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/robfig/go-cache v0.0.0-20130306151617-9fc39e0dbf62 h1:pyecQtsPmlkCsMkYhT5iZ+sUXuwee+OvfuJjinEA3ko=
github.com/robfig/go-cache v0.0.0-20130306151617-9fc39e0dbf62/go.mod h1:65XQgovT59RWatovFwnwocoUxiI/eENTnOY5GK3STuY=
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
Expand All @@ -99,6 +107,8 @@ golang.org/x/arch v0.7.0 h1:pskyeJh/3AmoQ8CPE95vxHLqp1G1GfGNXTmcl9NEKTc=
golang.org/x/arch v0.7.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f h1:99ci1mjWVBWwJiEKYY6jWa4d2nTQVIEhZIptnrVb1XY=
golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI=
golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
Expand Down
28 changes: 28 additions & 0 deletions server/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -1394,3 +1394,31 @@ func (b *BaseRouter) addJobRoutes() {
c.JSON(http.StatusOK, *response)
})
}

func (b *BaseRouter) addTaskRoutes() {
task := b.rg.Group("/task").Use(AuthRequired(b.db), AdminRequired())

task.GET("/", func(c *gin.Context) {
response := getAllTasks()
c.JSON(http.StatusOK, response)
})

task.PUT(":name", func(c *gin.Context) {
if c.Param("name") == "" {
c.JSON(http.StatusBadRequest, ErrorResponse{Error: "no task name provided"})
return
}
var rr TaskRescheduleRequest
err := c.ShouldBindJSON(&rr)
if err == nil {
err := rescheduleTask(c.Param("name"), rr)
if err != nil {
c.JSON(http.StatusInternalServerError, ErrorResponse{Error: err.Error()})
return
}
c.Status(http.StatusOK)
return
}
c.AbortWithStatusJSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
})
}
171 changes: 154 additions & 17 deletions server/tasks.go
Original file line number Diff line number Diff line change
@@ -1,36 +1,173 @@
package main

import (
"errors"
"log/slog"
"time"

"github.com/go-co-op/gocron/v2"
"gorm.io/gorm"
)

type TaskRescheduleRequest struct {
// Number of seconds inbetween each run of this task.
Seconds int `json:"seconds" binding:"required"`
}

type AllTasksResponse struct {
// The tasks name.
Name string `json:"name"`
// When this task will next run.
NextRun time.Time `json:"nextRun"`
// Current schedule for this task (seconds).
Seconds int `json:"seconds"`
}

type TaskFunc struct {
// Task function.
f func()
// Default duration (schedule) for task.
dd time.Duration
}

var taskScheduler gocron.Scheduler

// All task functions are stored here so when updating (rescheduling)
// a job, we can give it this function again.
// Doesn't seem to be a way to only update the schedule of a job,
// the .Update func wants the whole definition again.
//
// All funcs simply call a cleaning/routine method where the rest of the
// related code lives so it's kept tidy.
var taskFuncs map[string]TaskFunc

// Setup recurring tasks (eg cleanup every x mins)
func setupTasks(db *gorm.DB) {
go setupMinutelyTasks(db)
go setupDailyTasks(db)
ts, err := gocron.NewScheduler()
if err != nil {
slog.Error("SetupTasks: Failed to create new scheduler!", "error", err)
return
}
taskScheduler = ts

// Define all task funcs.
taskFuncs = map[string]TaskFunc{
"Cleanup Tokens": {
f: func() {
cleanupTokens(db)
},
dd: 60 * time.Second,
},
"Refresh Arr Queues": {
f: func() {
refreshArrQueues()
},
dd: 60 * time.Second,
},
"Cleanup Images": {
f: func() {
cleanupImages(db)
},
dd: 24 * time.Hour,
},
}

// Add all jobs to scheduler.
for k, v := range taskFuncs {
err = addTaskToScheduler(k, v.dd)
if err != nil {
slog.Error("SetupTasks: Failed to add new job", "job", k, "err", err)
}
}

taskScheduler.Start()
slog.Info("SetupTasks: Jobs created and scheduler started.")
}

// Gets schedule from config, or `defaultDur` if not manually configured.
func getTaskSeconds(name string, defaultDur time.Duration) time.Duration {
s := defaultDur
if Config.TASK_SCHEDULE[name] != 0 {
s = time.Duration(Config.TASK_SCHEDULE[name]) * time.Second
}
return s
}

func setupMinutelyTasks(db *gorm.DB) {
taskRunInterval := 1 * time.Minute
ticker := time.NewTicker(taskRunInterval)
defer ticker.Stop()
// Add new job to scheduler.
func addTaskToScheduler(name string, defaultDur time.Duration) error {
s := getTaskSeconds(name, defaultDur)
_, err := taskScheduler.NewJob(
gocron.DurationJob(s),
gocron.NewTask(taskFuncs[name].f),
gocron.WithName(name),
)
slog.Debug("addTaskToScheduler: Job added.", "job_name", name, "duration_used", s, "duration_default", defaultDur)
return err
}

for range ticker.C {
// Runs funcs that are in the place where we are cleaning.
// Bit cleaner and we can keep the related code close to its home.
cleanupTokens(db)
refreshArrQueues()
// Get all tasks in a consumable format.
func getAllTasks() []AllTasksResponse {
jobs := []AllTasksResponse{}
for _, j := range taskScheduler.Jobs() {
j2a := AllTasksResponse{
Name: j.Name(),
}
nextRun, err := j.NextRun()
if err != nil {
slog.Error("getAllTasks: Failed to get next run time for a job.", "job_name", j2a.Name)
} else {
j2a.NextRun = nextRun
}
j2a.Seconds = int(getTaskSeconds(j2a.Name, taskFuncs[j2a.Name].dd).Seconds())
jobs = append(jobs, j2a)
}
return jobs
}

func setupDailyTasks(db *gorm.DB) {
taskRunInterval := 24 * time.Hour
ticker := time.NewTicker(taskRunInterval)
defer ticker.Stop()
// Get task (job) from scheduler by name.
func getTask(name string) *gocron.Job {
var job *gocron.Job
for _, j := range taskScheduler.Jobs() {
if j.Name() == name {
job = &j
break
}
}
return job
}

for range ticker.C {
cleanupImages(db)
// Reschedule a task by name.
func rescheduleTask(name string, req TaskRescheduleRequest) error {
if req.Seconds == 0 {
return errors.New("request has no seconds")
}
j := getTask(name)
if j == nil {
return errors.New("no task found")
}
// Update config
if Config.TASK_SCHEDULE == nil {
Config.TASK_SCHEDULE = map[string]int{}
}
Config.TASK_SCHEDULE[name] = req.Seconds
if err := writeConfig(); err != nil {
slog.Error("rescheduleTask: Failed to write updated config to file!", "error", err)
return errors.New("failed to write config")
}
// Update job in scheduler
_, err := taskScheduler.Update(
(*j).ID(),
gocron.DurationJob(
time.Duration(req.Seconds)*time.Second,
),
gocron.NewTask(
taskFuncs[name].f,
),
gocron.WithName(name),
)
if err != nil {
slog.Error("rescheduleTask: Failed to update job!", "error", err)
return errors.New("failed to update job")
}
return nil
}
1 change: 1 addition & 0 deletions server/watcharr.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ func main() {
br.addRadarrRoutes()
br.addArrRequestRoutes()
br.addJobRoutes()
br.addTaskRoutes()
br.rg.Static("/img", path.Join(DataPath, "img"))

go setupTasks(db)
Expand Down
2 changes: 1 addition & 1 deletion src/lib/settings/Setting.svelte
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script lang="ts">
export let title: string;
export let title: string = "";
export let desc: string = "";
export let row: boolean = false;
</script>
Expand Down
5 changes: 5 additions & 0 deletions src/lib/settings/SettingButton.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -45,5 +45,10 @@
&:hover {
border-style: solid;
}
:global(svg) {
width: 24px;
min-width: 24px;
}
}
</style>
21 changes: 21 additions & 0 deletions src/lib/util/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -346,3 +346,24 @@ export function toRelativeDate(d: Date): string {
}
return `${d.getDate()}${getOrdinalSuffix(d.getDate())} ${monthsShort[d.getMonth()]} ${d.getFullYear()}`;
}

/**
* To relative time (seconds, mins, hours).
* @param s Seconds.
*/
export function toRelativeTime(s: number) {
const totalMinutes = Math.floor(s / 60);
const hours = Math.floor(totalMinutes / 60);
const mins = totalMinutes % 60;
const secs = Math.floor(s % 60);
if (hours > 0) {
return `${hours} hours`;
}
if (mins > 0) {
return `${mins} minutes`;
}
if (secs > 0) {
return `${secs} seconds`;
}
return "now";
}
15 changes: 15 additions & 0 deletions src/routes/(app)/server/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import Stat from "@/lib/stats/Stat.svelte";
import TwitchModal from "./modals/TwitchModal.svelte";
import RegionDropDown from "@/lib/RegionDropDown.svelte";
import TaskScheduleModal from "./modals/TaskScheduleModal.svelte";
let serverConfig: ServerConfig;
let sonarrModalOpen = false;
Expand All @@ -32,6 +33,7 @@
let radarrServerEditing: RadarrSettings;
let radarrModalEditing = false;
let twitchModalOpen = false;
let taskScheduleModalOpen = false;
// Disabled vars for disabling inputs until api request completes
let signupDisabled = false;
let debugDisabled = false;
Expand Down Expand Up @@ -250,6 +252,19 @@
}}
/>
</Setting>
<Setting>
<SettingButton
title="Task Schedule"
desc="View and configure server task schedule."
icon={"arrow"}
onClick={() => {
taskScheduleModalOpen = true;
}}
/>
</Setting>
{#if taskScheduleModalOpen}
<TaskScheduleModal onClose={() => (taskScheduleModalOpen = false)}></TaskScheduleModal>
{/if}
<div>
<h3>Services</h3>
<h5 class="norm">
Expand Down
Loading

0 comments on commit 62ca498

Please sign in to comment.