Wires the scheduler into the cup application following the exact conventions of the Asynq queue integration. A new testmac/config/scheduler.go file provides GetSchedulerConfig() with enable flag, polling interval, and rate limit. A new testmac/register-scheduler.go file registers task handlers (send_email and flaky_task as samples) on the Schedulermux, creates the DB-backed store via core.ResolveGorm(), and launches the scheduler in a goroutine — only when EnableScheduler: true. The scheduler tables are migrated alongside other models in run-auto-migrations.go via db.AutoMigrate(&scheduler.QueueItem{}, &scheduler.ProcessedItem{}).

This commit is contained in:
Jose Cely 2026-06-13 18:59:06 -05:00
parent f4476d3e89
commit 3d051e9617
6 changed files with 203 additions and 0 deletions

31
config/scheduler.go Normal file
View file

@ -0,0 +1,31 @@
// Copyright (c) 2026 Jose Cely <me@jacs.guru>
// Use of this source code is governed by MIT-style
// license that can be found in the LICENSE file.
package config
import (
"time"
"git.smarteching.com/goffee/core"
)
// Retrieve the main config for the Simple Scheduler
func GetSchedulerConfig() core.SchedulerConfig {
//#####################################
//# Main configuration for ####
//# the Simple Scheduler (sche) ####
//#####################################
return core.SchedulerConfig{
// For enabling and disabling the simple scheduler system
// set to true to enable it, set to false to disable
EnableScheduler: false,
// How often the scheduler polls the database for new tasks
SchedulerInterval: 10 * time.Second,
// Max tasks to process per tick (0 = unlimited)
SchedulerRateLimit: 5,
}
}

View file

@ -0,0 +1,89 @@
// Copyright (c) 2025 Zeni Kim <zenik@smarteching.com>
// Use of this source code is governed by MIT-style
// license that can be found in the LICENSE file.
package controllers
import (
"context"
"encoding/json"
"fmt"
"git.smarteching.com/goffee/core"
)
// SchedulerSample demonstrates enqueuing tasks into the simple scheduler
// from a controller.
func SchedulerSample(c *core.Context) *core.Response {
store := core.ResolveSchedulerStore()
if store == nil {
return c.Response.SetStatusCode(500).Json(`{"message": "scheduler store not available"}`)
}
ctx := context.Background()
// ── Enqueue a "send_email" task ─────────────────────────────────────────
emailPayload, _ := json.Marshal(map[string]interface{}{
"to": "user@example.com",
"subject": "Hello from the scheduler!",
})
item1, err := store.Enqueue(ctx, "send_email", string(emailPayload), 0)
if err != nil {
c.GetLogger().Error(fmt.Sprintf("failed to enqueue send_email: %v", err))
return c.Response.SetStatusCode(500).Json(`{"message": "failed to enqueue send_email"}`)
}
c.GetLogger().Info(fmt.Sprintf("[scheduler] enqueued send_email task id=%d", item1.ID))
// ── Enqueue a "flaky_task" (demonstrates error recording) ───────────────
flakyPayload, _ := json.Marshal(map[string]interface{}{
"description": "This task will fail and the error will be recorded",
})
item2, err := store.Enqueue(ctx, "flaky_task", string(flakyPayload), 0)
if err != nil {
c.GetLogger().Error(fmt.Sprintf("failed to enqueue flaky_task: %v", err))
return c.Response.SetStatusCode(500).Json(`{"message": "failed to enqueue flaky_task"}`)
}
c.GetLogger().Info(fmt.Sprintf("[scheduler] enqueued flaky_task id=%d", item2.ID))
// ── Enqueue a high-priority "send_email" task ───────────────────────────
urgentPayload, _ := json.Marshal(map[string]interface{}{
"to": "urgent@example.com",
"subject": "URGENT: Priority message",
})
item3, err := store.Enqueue(ctx, "send_email", string(urgentPayload), 10)
if err != nil {
c.GetLogger().Error(fmt.Sprintf("failed to enqueue urgent send_email: %v", err))
return c.Response.SetStatusCode(500).Json(`{"message": "failed to enqueue urgent send_email"}`)
}
c.GetLogger().Info(fmt.Sprintf("[scheduler] enqueued high-priority send_email task id=%d", item3.ID))
message := fmt.Sprintf(`{"message": "Enqueued 3 scheduler tasks (send_email x2, flaky_task x1)", "tasks": [%d, %d, %d]}`,
item1.ID, item2.ID, item3.ID)
return c.Response.Json(message)
}
// SchedulerSemaphoreToggle flips the scheduler semaphore state.
// If it was green it becomes red (blocking execution), and vice versa.
// Useful for testing the semaphore kill switch without restarting the app.
func SchedulerSemaphoreToggle(c *core.Context) *core.Response {
wasGreen := core.SchedulerSemaphoreIsGreen()
if wasGreen {
core.SchedulerSemaphoreSetRed()
} else {
core.SchedulerSemaphoreSetGreen()
}
newState := "GREEN"
if !core.SchedulerSemaphoreIsGreen() {
newState = "RED"
}
c.GetLogger().Info(fmt.Sprintf("[scheduler] semaphore toggled: was %s → now %s",
map[bool]string{true: "GREEN", false: "RED"}[wasGreen], newState))
message := fmt.Sprintf(`{"message": "Scheduler semaphore flipped", "was": "%s", "now": "%s"}`,
map[bool]string{true: "GREEN", false: "RED"}[wasGreen], newState)
return c.Response.Json(message)
}

View file

@ -65,6 +65,9 @@ func main() {
if config.GetQueueConfig().EnableQueue == true {
registerQueues()
}
if config.GetSchedulerConfig().EnableScheduler == true {
registerScheduler()
}
if config.GetGormConfig().EnableGorm == true {
RunAutoMigrations()
}

71
register-scheduler.go Normal file
View file

@ -0,0 +1,71 @@
// Copyright (c) 2025 Zeni Kim <zenik@smarteching.com>
// Use of this source code is governed by MIT-style
// license that can be found in the LICENSE file.
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"time"
"git.smarteching.com/goffee/core"
"git.smarteching.com/goffee/core/scheduler"
"git.smarteching.com/goffee/cup/config"
)
// ─── Scheduler task handlers ─────────────────────────────────────────────────
// handleSendEmail processes a send_email scheduler task.
func handleSendEmail(ctx context.Context, item *scheduler.QueueItem) error {
var payload struct {
To string `json:"to"`
Subject string `json:"subject"`
}
if err := json.Unmarshal([]byte(item.Payload), &payload); err != nil {
return fmt.Errorf("invalid send_email payload: %w", err)
}
log.Printf("[scheduler] [send_email] → to=%s subject=%q", payload.To, payload.Subject)
time.Sleep(50 * time.Millisecond) // simulate I/O
return nil
}
// handleFlakyTask demonstrates a task that always fails.
func handleFlakyTask(ctx context.Context, item *scheduler.QueueItem) error {
log.Printf("[scheduler] [flaky_task] attempting... payload=%s", item.Payload)
time.Sleep(100 * time.Millisecond)
return fmt.Errorf("external API returned 503 — simulated failure for demo")
}
// ─── Registration ────────────────────────────────────────────────────────────
// Register simple scheduler
func registerScheduler() {
var sched = new(core.Schedulermux)
sched.SchedulerInit()
//########################################
//# scheduler task registration #####
//########################################
// Register your scheduler task handlers here ...
sched.AddWork("send_email", handleSendEmail)
sched.AddWork("flaky_task", handleFlakyTask)
//########################################
// Set the store (DB-backed) and start
//########################################
// Create the store using the application's GORM DB
st, err := scheduler.NewStore(core.ResolveGorm())
if err != nil {
panic(fmt.Sprintf("scheduler: failed to create store: %v", err))
}
sched.SetStore(st)
// Start scheduler server in background, DO NOT TOUCH
go sched.RunScheduler(config.GetSchedulerConfig())
}

View file

@ -31,6 +31,10 @@ func registerRoutes() {
// queue sample route
controller.Get("/queuesample", controllers.Queuesample)
// scheduler sample routes
controller.Get("/schedsample", controllers.SchedulerSample)
controller.Get("/schedsemaphore", controllers.SchedulerSemaphoreToggle)
// Uncomment the lines below to enable user administration
controller.Get("/admin/users", controllers.AdminUsersList)
controller.Post("/admin/users", controllers.AdminUsersList)

View file

@ -9,6 +9,7 @@ import (
"errors"
"git.smarteching.com/goffee/core"
"git.smarteching.com/goffee/core/scheduler"
"git.smarteching.com/goffee/cup/models"
"git.smarteching.com/goffee/cup/utils"
"gorm.io/gorm"
@ -26,6 +27,10 @@ func RunAutoMigrations() {
db.AutoMigrate(&models.RolePermission{})
db.AutoMigrate(&models.Permission{})
// Simple scheduler (sche) tables — enabled when scheduler is active
db.AutoMigrate(&scheduler.QueueItem{})
db.AutoMigrate(&scheduler.ProcessedItem{})
// End your auto migrations
// Create seed data data, DO NOT TOUCH