diff --git a/config/scheduler.go b/config/scheduler.go new file mode 100644 index 0000000..070f619 --- /dev/null +++ b/config/scheduler.go @@ -0,0 +1,31 @@ +// Copyright (c) 2026 Jose Cely +// 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, + } +} diff --git a/controllers/schedsample.go b/controllers/schedsample.go new file mode 100644 index 0000000..44856d0 --- /dev/null +++ b/controllers/schedsample.go @@ -0,0 +1,89 @@ +// Copyright (c) 2025 Zeni Kim +// 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) +} diff --git a/main.go b/main.go index 5fac06e..cdf6a34 100644 --- a/main.go +++ b/main.go @@ -65,6 +65,9 @@ func main() { if config.GetQueueConfig().EnableQueue == true { registerQueues() } + if config.GetSchedulerConfig().EnableScheduler == true { + registerScheduler() + } if config.GetGormConfig().EnableGorm == true { RunAutoMigrations() } diff --git a/register-scheduler.go b/register-scheduler.go new file mode 100644 index 0000000..8067a76 --- /dev/null +++ b/register-scheduler.go @@ -0,0 +1,71 @@ +// Copyright (c) 2025 Zeni Kim +// 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()) +} diff --git a/routes.go b/routes.go index b6b1f3b..e434b33 100644 --- a/routes.go +++ b/routes.go @@ -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) diff --git a/run-auto-migrations.go b/run-auto-migrations.go index 996ae15..9a84d87 100644 --- a/run-auto-migrations.go +++ b/run-auto-migrations.go @@ -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