Implement tests scheduler
This commit is contained in:
parent
ef6b8ea294
commit
b2616c5c3a
9 changed files with 1155 additions and 0 deletions
231
internal/api/controller/testschedule_controller.go
Normal file
231
internal/api/controller/testschedule_controller.go
Normal file
|
|
@ -0,0 +1,231 @@
|
|||
// This file is part of the happyDomain (R) project.
|
||||
// Copyright (c) 2020-2026 happyDomain
|
||||
// Authors: Pierre-Olivier Mercier, et al.
|
||||
//
|
||||
// This program is offered under a commercial and under the AGPL license.
|
||||
// For commercial licensing, contact us at <contact@happydomain.org>.
|
||||
//
|
||||
// For AGPL licensing:
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package controller
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"git.happydns.org/happyDomain/internal/api/middleware"
|
||||
"git.happydns.org/happyDomain/model"
|
||||
)
|
||||
|
||||
// CheckerScheduleController handles test schedule operations
|
||||
type CheckerScheduleController struct {
|
||||
testScheduleUC happydns.CheckerScheduleUsecase
|
||||
}
|
||||
|
||||
func NewCheckerScheduleController(testScheduleUC happydns.CheckerScheduleUsecase) *CheckerScheduleController {
|
||||
return &CheckerScheduleController{
|
||||
testScheduleUC: testScheduleUC,
|
||||
}
|
||||
}
|
||||
|
||||
// ListCheckerSchedules retrieves schedules for the authenticated user
|
||||
//
|
||||
// @Summary List test schedules
|
||||
// @Description Retrieves test schedules for the authenticated user with optional pagination
|
||||
// @Tags test-schedules
|
||||
// @Produce json
|
||||
// @Param limit query int false "Maximum number of schedules to return (0 = all)"
|
||||
// @Param offset query int false "Number of schedules to skip (default: 0)"
|
||||
// @Success 200 {array} happydns.CheckerSchedule
|
||||
// @Failure 500 {object} happydns.ErrorResponse
|
||||
// @Router /plugins/tests/schedules [get]
|
||||
func (tc *CheckerScheduleController) ListCheckerSchedules(c *gin.Context) {
|
||||
user := middleware.MyUser(c)
|
||||
|
||||
schedules, err := tc.testScheduleUC.ListUserSchedules(user.Id)
|
||||
if err != nil {
|
||||
middleware.ErrorResponse(c, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Apply pagination
|
||||
limit := 0
|
||||
offset := 0
|
||||
fmt.Sscanf(c.Query("limit"), "%d", &limit)
|
||||
fmt.Sscanf(c.Query("offset"), "%d", &offset)
|
||||
|
||||
if offset > len(schedules) {
|
||||
offset = len(schedules)
|
||||
}
|
||||
schedules = schedules[offset:]
|
||||
if limit > 0 && len(schedules) > limit {
|
||||
schedules = schedules[:limit]
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, schedules)
|
||||
}
|
||||
|
||||
// CreateCheckerSchedule creates a new test schedule
|
||||
//
|
||||
// @Summary Create test schedule
|
||||
// @Description Creates a new test schedule for the authenticated user
|
||||
// @Tags test-schedules
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param body body happydns.CheckerSchedule true "Check schedule to create"
|
||||
// @Success 201 {object} happydns.CheckerSchedule
|
||||
// @Failure 400 {object} happydns.ErrorResponse
|
||||
// @Failure 500 {object} happydns.ErrorResponse
|
||||
// @Router /plugins/tests/schedules [post]
|
||||
func (tc *CheckerScheduleController) CreateCheckerSchedule(c *gin.Context) {
|
||||
user := middleware.MyUser(c)
|
||||
|
||||
var schedule happydns.CheckerSchedule
|
||||
if err := c.ShouldBindJSON(&schedule); err != nil {
|
||||
middleware.ErrorResponse(c, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Set user ID
|
||||
schedule.OwnerId = user.Id
|
||||
|
||||
if err := tc.testScheduleUC.CreateSchedule(&schedule); err != nil {
|
||||
middleware.ErrorResponse(c, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, schedule)
|
||||
}
|
||||
|
||||
// GetCheckerSchedule retrieves a specific schedule
|
||||
//
|
||||
// @Summary Get test schedule
|
||||
// @Description Retrieves a specific test schedule by ID
|
||||
// @Tags test-schedules
|
||||
// @Produce json
|
||||
// @Param schedule_id path string true "Schedule ID"
|
||||
// @Success 200 {object} happydns.CheckerSchedule
|
||||
// @Failure 404 {object} happydns.ErrorResponse
|
||||
// @Failure 500 {object} happydns.ErrorResponse
|
||||
// @Router /plugins/tests/schedules/{schedule_id} [get]
|
||||
func (tc *CheckerScheduleController) GetCheckerSchedule(c *gin.Context) {
|
||||
user := middleware.MyUser(c)
|
||||
scheduleIdStr := c.Param("schedule_id")
|
||||
|
||||
scheduleId, err := happydns.NewIdentifierFromString(scheduleIdStr)
|
||||
if err != nil {
|
||||
middleware.ErrorResponse(c, http.StatusBadRequest, fmt.Errorf("invalid schedule ID"))
|
||||
return
|
||||
}
|
||||
|
||||
// Verify ownership
|
||||
if err := tc.testScheduleUC.ValidateScheduleOwnership(scheduleId, user.Id); err != nil {
|
||||
middleware.ErrorResponse(c, http.StatusForbidden, err)
|
||||
return
|
||||
}
|
||||
|
||||
schedule, err := tc.testScheduleUC.GetSchedule(scheduleId)
|
||||
if err != nil {
|
||||
middleware.ErrorResponse(c, http.StatusNotFound, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, schedule)
|
||||
}
|
||||
|
||||
// UpdateCheckerSchedule updates an existing schedule
|
||||
//
|
||||
// @Summary Update test schedule
|
||||
// @Description Updates an existing test schedule
|
||||
// @Tags test-schedules
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param schedule_id path string true "Schedule ID"
|
||||
// @Param body body happydns.CheckerSchedule true "Updated schedule"
|
||||
// @Success 200 {object} happydns.CheckerSchedule
|
||||
// @Failure 400 {object} happydns.ErrorResponse
|
||||
// @Failure 404 {object} happydns.ErrorResponse
|
||||
// @Failure 500 {object} happydns.ErrorResponse
|
||||
// @Router /plugins/tests/schedules/{schedule_id} [put]
|
||||
func (tc *CheckerScheduleController) UpdateCheckerSchedule(c *gin.Context) {
|
||||
user := middleware.MyUser(c)
|
||||
scheduleIdStr := c.Param("schedule_id")
|
||||
|
||||
scheduleId, err := happydns.NewIdentifierFromString(scheduleIdStr)
|
||||
if err != nil {
|
||||
middleware.ErrorResponse(c, http.StatusBadRequest, fmt.Errorf("invalid schedule ID"))
|
||||
return
|
||||
}
|
||||
|
||||
// Verify ownership
|
||||
if err := tc.testScheduleUC.ValidateScheduleOwnership(scheduleId, user.Id); err != nil {
|
||||
middleware.ErrorResponse(c, http.StatusForbidden, err)
|
||||
return
|
||||
}
|
||||
|
||||
var schedule happydns.CheckerSchedule
|
||||
if err := c.ShouldBindJSON(&schedule); err != nil {
|
||||
middleware.ErrorResponse(c, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Ensure ID matches
|
||||
schedule.Id = scheduleId
|
||||
schedule.OwnerId = user.Id
|
||||
|
||||
if err := tc.testScheduleUC.UpdateSchedule(&schedule); err != nil {
|
||||
middleware.ErrorResponse(c, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, schedule)
|
||||
}
|
||||
|
||||
// DeleteCheckerSchedule deletes a schedule
|
||||
//
|
||||
// @Summary Delete test schedule
|
||||
// @Description Deletes a test schedule
|
||||
// @Tags test-schedules
|
||||
// @Produce json
|
||||
// @Param schedule_id path string true "Schedule ID"
|
||||
// @Success 204 "No Content"
|
||||
// @Failure 404 {object} happydns.ErrorResponse
|
||||
// @Failure 500 {object} happydns.ErrorResponse
|
||||
// @Router /plugins/tests/schedules/{schedule_id} [delete]
|
||||
func (tc *CheckerScheduleController) DeleteCheckerSchedule(c *gin.Context) {
|
||||
user := middleware.MyUser(c)
|
||||
scheduleIdStr := c.Param("schedule_id")
|
||||
|
||||
scheduleId, err := happydns.NewIdentifierFromString(scheduleIdStr)
|
||||
if err != nil {
|
||||
middleware.ErrorResponse(c, http.StatusBadRequest, fmt.Errorf("invalid schedule ID"))
|
||||
return
|
||||
}
|
||||
|
||||
// Verify ownership
|
||||
if err := tc.testScheduleUC.ValidateScheduleOwnership(scheduleId, user.Id); err != nil {
|
||||
middleware.ErrorResponse(c, http.StatusForbidden, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := tc.testScheduleUC.DeleteSchedule(scheduleId); err != nil {
|
||||
middleware.ErrorResponse(c, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
47
internal/api/route/checkschedule.go
Normal file
47
internal/api/route/checkschedule.go
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
// This file is part of the happyDomain (R) project.
|
||||
// Copyright (c) 2020-2026 happyDomain
|
||||
// Authors: Pierre-Olivier Mercier, et al.
|
||||
//
|
||||
// This program is offered under a commercial and under the AGPL license.
|
||||
// For commercial licensing, contact us at <contact@happydomain.org>.
|
||||
//
|
||||
// For AGPL licensing:
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package route
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"git.happydns.org/happyDomain/internal/api/controller"
|
||||
"git.happydns.org/happyDomain/model"
|
||||
)
|
||||
|
||||
// DeclareTestScheduleRoutes declares test schedule management routes
|
||||
func DeclareTestScheduleRoutes(router *gin.RouterGroup, checkerScheduleUC happydns.CheckerScheduleUsecase) {
|
||||
sc := controller.NewCheckerScheduleController(checkerScheduleUC)
|
||||
|
||||
schedulesRoutes := router.Group("/plugins/tests/schedules")
|
||||
{
|
||||
schedulesRoutes.GET("", sc.ListCheckerSchedules)
|
||||
schedulesRoutes.POST("", sc.CreateCheckerSchedule)
|
||||
|
||||
scheduleRoutes := schedulesRoutes.Group("/:schedule_id")
|
||||
{
|
||||
scheduleRoutes.GET("", sc.GetCheckerSchedule)
|
||||
scheduleRoutes.PUT("", sc.UpdateCheckerSchedule)
|
||||
scheduleRoutes.DELETE("", sc.DeleteCheckerSchedule)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -108,6 +108,7 @@ func DeclareRoutes(cfg *happydns.Options, router *gin.RouterGroup, dep Dependenc
|
|||
DeclareProviderRoutes(apiAuthRoutes, dep.Provider)
|
||||
DeclareProviderSettingsRoutes(apiAuthRoutes, dep.ProviderSettings)
|
||||
DeclareRecordRoutes(apiAuthRoutes)
|
||||
DeclareTestScheduleRoutes(apiAuthRoutes, dep.CheckerSchedule)
|
||||
DeclareUsersRoutes(apiAuthRoutes, dep.User, lc)
|
||||
DeclareSessionRoutes(apiAuthRoutes, dep.Session)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ type Usecases struct {
|
|||
authUser happydns.AuthUserUsecase
|
||||
checker happydns.CheckerUsecase
|
||||
checkResult happydns.CheckResultUsecase
|
||||
checkerSchedule happydns.CheckerScheduleUsecase
|
||||
domain happydns.DomainUsecase
|
||||
domainLog happydns.DomainLogUsecase
|
||||
provider happydns.ProviderUsecase
|
||||
|
|
@ -85,6 +86,7 @@ type App struct {
|
|||
router *gin.Engine
|
||||
srv *http.Server
|
||||
store storage.Storage
|
||||
checkScheduler happydns.SchedulerUsecase
|
||||
usecases Usecases
|
||||
}
|
||||
|
||||
|
|
@ -102,6 +104,7 @@ func NewApp(cfg *happydns.Options) *App {
|
|||
}
|
||||
app.initUsecases()
|
||||
app.initCaptcha()
|
||||
app.initCheckScheduler()
|
||||
app.setupRouter()
|
||||
|
||||
return app
|
||||
|
|
@ -120,6 +123,7 @@ func NewAppWithStorage(cfg *happydns.Options, store storage.Storage) *App {
|
|||
}
|
||||
app.initUsecases()
|
||||
app.initCaptcha()
|
||||
app.initCheckScheduler()
|
||||
app.setupRouter()
|
||||
|
||||
return app
|
||||
|
|
@ -193,6 +197,20 @@ func (app *App) initInsights() {
|
|||
}
|
||||
}
|
||||
|
||||
func (app *App) initCheckScheduler() {
|
||||
if app.cfg.DisableScheduler {
|
||||
// Use a disabled scheduler that returns clear errors
|
||||
app.checkScheduler = &disabledScheduler{}
|
||||
return
|
||||
}
|
||||
|
||||
app.checkScheduler = newCheckScheduler(
|
||||
app.cfg,
|
||||
app.store,
|
||||
app.usecases.checker,
|
||||
)
|
||||
}
|
||||
|
||||
func (app *App) initUsecases() {
|
||||
sessionService := sessionUC.NewService(app.store)
|
||||
authUserService := authuserUC.NewAuthUserUsecases(app.cfg, app.mailer, app.store, sessionService)
|
||||
|
|
@ -222,6 +240,7 @@ func (app *App) initUsecases() {
|
|||
app.usecases.session = sessionService
|
||||
app.usecases.checker = checkUC.NewCheckerUsecase(app.cfg, app.store)
|
||||
app.usecases.checkResult = checkresultUC.NewCheckResultUsecase(app.store, app.cfg)
|
||||
app.usecases.checkerSchedule = checkresultUC.NewCheckScheduleUsecase(app.store, app.cfg)
|
||||
|
||||
app.usecases.orchestrator = orchestrator.NewOrchestrator(
|
||||
domainLogService,
|
||||
|
|
@ -292,6 +311,8 @@ func (app *App) Start() {
|
|||
go app.insights.Run()
|
||||
}
|
||||
|
||||
go app.checkScheduler.Run()
|
||||
|
||||
log.Printf("Public interface listening on %s\n", app.cfg.Bind)
|
||||
if err := app.srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
log.Fatalf("listen: %s\n", err)
|
||||
|
|
@ -317,4 +338,6 @@ func (app *App) Stop() {
|
|||
if app.failureTracker != nil {
|
||||
app.failureTracker.Close()
|
||||
}
|
||||
|
||||
app.checkScheduler.Close()
|
||||
}
|
||||
|
|
|
|||
534
internal/app/checkscheduler.go
Normal file
534
internal/app/checkscheduler.go
Normal file
|
|
@ -0,0 +1,534 @@
|
|||
// This file is part of the happyDomain (R) project.
|
||||
// Copyright (c) 2020-2026 happyDomain
|
||||
// Authors: Pierre-Olivier Mercier, et al.
|
||||
//
|
||||
// This program is offered under a commercial and under the AGPL license.
|
||||
// For commercial licensing, contact us at <contact@happydomain.org>.
|
||||
//
|
||||
// For AGPL licensing:
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"container/heap"
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"runtime"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.happydns.org/happyDomain/internal/storage"
|
||||
"git.happydns.org/happyDomain/internal/usecase/checkresult"
|
||||
"git.happydns.org/happyDomain/model"
|
||||
)
|
||||
|
||||
const (
|
||||
SchedulerCheckInterval = 1 * time.Minute // How often to check for due tests
|
||||
SchedulerCleanupInterval = 24 * time.Hour // How often to clean up old executions
|
||||
CheckExecutionTimeout = 5 * time.Minute // Max time for a single test
|
||||
MaxRetries = 3 // Max retry attempts for failed tests
|
||||
)
|
||||
|
||||
// Priority levels for test execution queue
|
||||
const (
|
||||
PriorityOnDemand = iota // On-demand tests (highest priority)
|
||||
PriorityOverdue // Overdue scheduled tests
|
||||
PriorityScheduled // Regular scheduled tests
|
||||
)
|
||||
|
||||
// checkScheduler manages background test execution
|
||||
type checkScheduler struct {
|
||||
cfg *happydns.Options
|
||||
store storage.Storage
|
||||
checkerUsecase happydns.CheckerUsecase
|
||||
resultUsecase *checkresult.CheckResultUsecase
|
||||
scheduleUsecase *checkresult.CheckScheduleUsecase
|
||||
stop chan struct{} // closed to stop the main Run loop
|
||||
stopWorkers chan struct{} // closed to stop all workers simultaneously
|
||||
runNowChan chan *queueItem // on-demand items routed through the main loop
|
||||
workAvail chan struct{} // non-blocking signals that queue has new work
|
||||
queue *priorityQueue
|
||||
activeExecutions map[string]*activeExecution
|
||||
workers []*worker
|
||||
mu sync.RWMutex
|
||||
wg sync.WaitGroup
|
||||
}
|
||||
|
||||
// activeExecution tracks a running test execution
|
||||
type activeExecution struct {
|
||||
execution *happydns.CheckExecution
|
||||
cancel context.CancelFunc
|
||||
startTime time.Time
|
||||
}
|
||||
|
||||
// queueItem represents a test execution request in the queue
|
||||
type queueItem struct {
|
||||
schedule *happydns.CheckerSchedule
|
||||
execution *happydns.CheckExecution
|
||||
priority int
|
||||
queuedAt time.Time
|
||||
retries int
|
||||
}
|
||||
|
||||
// --- container/heap implementation for priorityQueue ---
|
||||
|
||||
// priorityHeap is the underlying heap, ordered by priority then arrival time.
|
||||
type priorityHeap []*queueItem
|
||||
|
||||
func (h priorityHeap) Len() int { return len(h) }
|
||||
func (h priorityHeap) Less(i, j int) bool {
|
||||
if h[i].priority != h[j].priority {
|
||||
return h[i].priority < h[j].priority
|
||||
}
|
||||
return h[i].queuedAt.Before(h[j].queuedAt)
|
||||
}
|
||||
func (h priorityHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] }
|
||||
func (h *priorityHeap) Push(x any) { *h = append(*h, x.(*queueItem)) }
|
||||
func (h *priorityHeap) Pop() any {
|
||||
old := *h
|
||||
n := len(old)
|
||||
x := old[n-1]
|
||||
old[n-1] = nil // avoid memory leak
|
||||
*h = old[:n-1]
|
||||
return x
|
||||
}
|
||||
|
||||
// priorityQueue is a thread-safe min-heap of queueItems.
|
||||
type priorityQueue struct {
|
||||
h priorityHeap
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func newPriorityQueue() *priorityQueue {
|
||||
pq := &priorityQueue{}
|
||||
heap.Init(&pq.h)
|
||||
return pq
|
||||
}
|
||||
|
||||
// Push adds an item to the queue.
|
||||
func (q *priorityQueue) Push(item *queueItem) {
|
||||
q.mu.Lock()
|
||||
defer q.mu.Unlock()
|
||||
heap.Push(&q.h, item)
|
||||
}
|
||||
|
||||
// Pop removes and returns the highest-priority item, or nil if empty.
|
||||
func (q *priorityQueue) Pop() *queueItem {
|
||||
q.mu.Lock()
|
||||
defer q.mu.Unlock()
|
||||
if q.h.Len() == 0 {
|
||||
return nil
|
||||
}
|
||||
return heap.Pop(&q.h).(*queueItem)
|
||||
}
|
||||
|
||||
// Len returns the queue length.
|
||||
func (q *priorityQueue) Len() int {
|
||||
q.mu.Lock()
|
||||
defer q.mu.Unlock()
|
||||
return q.h.Len()
|
||||
}
|
||||
|
||||
// worker processes tests from the queue
|
||||
type worker struct {
|
||||
id int
|
||||
scheduler *checkScheduler
|
||||
}
|
||||
|
||||
// disabledScheduler is a no-op implementation used when scheduler is disabled
|
||||
type disabledScheduler struct{}
|
||||
|
||||
func (d *disabledScheduler) Run() {}
|
||||
func (d *disabledScheduler) Close() {}
|
||||
|
||||
// TriggerOnDemandCheck returns an error indicating the scheduler is disabled
|
||||
func (d *disabledScheduler) TriggerOnDemandCheck(checkName string, targetType happydns.CheckScopeType, targetId happydns.Identifier, userId happydns.Identifier, options happydns.CheckerOptions) (happydns.Identifier, error) {
|
||||
return happydns.Identifier{}, fmt.Errorf("test scheduler is disabled in configuration")
|
||||
}
|
||||
|
||||
// newCheckScheduler creates a new test scheduler
|
||||
func newCheckScheduler(
|
||||
cfg *happydns.Options,
|
||||
store storage.Storage,
|
||||
checkerUsecase happydns.CheckerUsecase,
|
||||
) *checkScheduler {
|
||||
numWorkers := cfg.TestWorkers
|
||||
if numWorkers <= 0 {
|
||||
numWorkers = runtime.NumCPU()
|
||||
}
|
||||
|
||||
scheduler := &checkScheduler{
|
||||
cfg: cfg,
|
||||
store: store,
|
||||
checkerUsecase: checkerUsecase,
|
||||
resultUsecase: checkresult.NewCheckResultUsecase(store, cfg),
|
||||
scheduleUsecase: checkresult.NewCheckScheduleUsecase(store, cfg),
|
||||
stop: make(chan struct{}),
|
||||
stopWorkers: make(chan struct{}),
|
||||
runNowChan: make(chan *queueItem, 100),
|
||||
workAvail: make(chan struct{}, numWorkers),
|
||||
queue: newPriorityQueue(),
|
||||
activeExecutions: make(map[string]*activeExecution),
|
||||
workers: make([]*worker, numWorkers),
|
||||
}
|
||||
|
||||
for i := 0; i < numWorkers; i++ {
|
||||
scheduler.workers[i] = &worker{
|
||||
id: i,
|
||||
scheduler: scheduler,
|
||||
}
|
||||
}
|
||||
|
||||
return scheduler
|
||||
}
|
||||
|
||||
// enqueue pushes an item to the priority queue and wakes one idle worker.
|
||||
func (s *checkScheduler) enqueue(item *queueItem) {
|
||||
s.queue.Push(item)
|
||||
select {
|
||||
case s.workAvail <- struct{}{}:
|
||||
default:
|
||||
// All workers are already busy or already notified; they will drain
|
||||
// the queue on their own after finishing the current item.
|
||||
}
|
||||
}
|
||||
|
||||
// Close stops the scheduler and waits for all workers to finish.
|
||||
func (s *checkScheduler) Close() {
|
||||
log.Println("Stopping test scheduler...")
|
||||
|
||||
// Unblock the main Run loop.
|
||||
close(s.stop)
|
||||
|
||||
// Unblock all workers simultaneously.
|
||||
close(s.stopWorkers)
|
||||
|
||||
// Cancel all active test executions.
|
||||
s.mu.Lock()
|
||||
for _, exec := range s.activeExecutions {
|
||||
exec.cancel()
|
||||
}
|
||||
s.mu.Unlock()
|
||||
|
||||
// Wait for all workers to finish their current item.
|
||||
s.wg.Wait()
|
||||
|
||||
log.Println("Check scheduler stopped")
|
||||
}
|
||||
|
||||
// Run starts the scheduler main loop. It must not be called more than once.
|
||||
func (s *checkScheduler) Run() {
|
||||
log.Printf("Starting test scheduler with %d workers...\n", len(s.workers))
|
||||
|
||||
// Start workers
|
||||
for _, w := range s.workers {
|
||||
s.wg.Add(1)
|
||||
go w.run(&s.wg)
|
||||
}
|
||||
|
||||
// Main scheduling loop
|
||||
checkTicker := time.NewTicker(SchedulerCheckInterval)
|
||||
cleanupTicker := time.NewTicker(SchedulerCleanupInterval)
|
||||
defer checkTicker.Stop()
|
||||
defer cleanupTicker.Stop()
|
||||
|
||||
// Initial check
|
||||
s.checkSchedules()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-checkTicker.C:
|
||||
s.checkSchedules()
|
||||
|
||||
case <-cleanupTicker.C:
|
||||
s.cleanup()
|
||||
|
||||
case item := <-s.runNowChan:
|
||||
s.enqueue(item)
|
||||
|
||||
case <-s.stop:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// checkSchedules checks for due tests and queues them
|
||||
func (s *checkScheduler) checkSchedules() {
|
||||
dueSchedules, err := s.scheduleUsecase.ListDueSchedules()
|
||||
if err != nil {
|
||||
log.Printf("Error listing due schedules: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
for _, schedule := range dueSchedules {
|
||||
// Determine priority based on how overdue the test is
|
||||
priority := PriorityScheduled
|
||||
if schedule.NextRun.Add(schedule.Interval).Before(now) {
|
||||
priority = PriorityOverdue
|
||||
}
|
||||
|
||||
// Create execution record
|
||||
execution := &happydns.CheckExecution{
|
||||
ScheduleId: &schedule.Id,
|
||||
CheckerName: schedule.CheckerName,
|
||||
OwnerId: schedule.OwnerId,
|
||||
TargetType: schedule.TargetType,
|
||||
TargetId: schedule.TargetId,
|
||||
Status: happydns.CheckExecutionPending,
|
||||
StartedAt: time.Now(),
|
||||
Options: schedule.Options,
|
||||
}
|
||||
|
||||
if err := s.resultUsecase.CreateCheckExecution(execution); err != nil {
|
||||
log.Printf("Error creating execution for schedule %s: %v\n", schedule.Id.String(), err)
|
||||
continue
|
||||
}
|
||||
|
||||
s.enqueue(&queueItem{
|
||||
schedule: schedule,
|
||||
execution: execution,
|
||||
priority: priority,
|
||||
queuedAt: now,
|
||||
retries: 0,
|
||||
})
|
||||
}
|
||||
|
||||
// Mark scheduler run
|
||||
if err := s.store.CheckSchedulerRun(); err != nil {
|
||||
log.Printf("Error marking scheduler run: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TriggerOnDemandCheck triggers an immediate test execution.
|
||||
// It creates the execution record synchronously (so the caller gets an ID back)
|
||||
// and then routes the item through runNowChan so the main loop controls
|
||||
// all queue insertions.
|
||||
func (s *checkScheduler) TriggerOnDemandCheck(checkerName string, targetType happydns.CheckScopeType, targetId happydns.Identifier, ownerId happydns.Identifier, options happydns.CheckerOptions) (happydns.Identifier, error) {
|
||||
schedule := &happydns.CheckerSchedule{
|
||||
CheckerName: checkerName,
|
||||
OwnerId: ownerId,
|
||||
TargetType: targetType,
|
||||
TargetId: targetId,
|
||||
Interval: 0, // On-demand, no interval
|
||||
Enabled: true,
|
||||
Options: options,
|
||||
}
|
||||
|
||||
execution := &happydns.CheckExecution{
|
||||
ScheduleId: nil,
|
||||
CheckerName: checkerName,
|
||||
OwnerId: ownerId,
|
||||
TargetType: targetType,
|
||||
TargetId: targetId,
|
||||
Status: happydns.CheckExecutionPending,
|
||||
StartedAt: time.Now(),
|
||||
Options: options,
|
||||
}
|
||||
|
||||
if err := s.resultUsecase.CreateCheckExecution(execution); err != nil {
|
||||
return happydns.Identifier{}, err
|
||||
}
|
||||
|
||||
item := &queueItem{
|
||||
schedule: schedule,
|
||||
execution: execution,
|
||||
priority: PriorityOnDemand,
|
||||
queuedAt: time.Now(),
|
||||
retries: 0,
|
||||
}
|
||||
|
||||
// Route through the main loop when possible; fall back to direct enqueue
|
||||
// if the channel is full so that the caller never blocks.
|
||||
select {
|
||||
case s.runNowChan <- item:
|
||||
default:
|
||||
s.enqueue(item)
|
||||
}
|
||||
|
||||
return execution.Id, nil
|
||||
}
|
||||
|
||||
// cleanup removes old execution records and expired test results
|
||||
func (s *checkScheduler) cleanup() {
|
||||
log.Println("Running scheduler cleanup...")
|
||||
|
||||
// Delete completed/failed execution records older than 7 days
|
||||
if err := s.resultUsecase.DeleteCompletedExecutions(7 * 24 * time.Hour); err != nil {
|
||||
log.Printf("Error cleaning up old executions: %v\n", err)
|
||||
}
|
||||
|
||||
// Delete test results older than the configured retention period
|
||||
if err := s.resultUsecase.CleanupOldResults(); err != nil {
|
||||
log.Printf("Error cleaning up old test results: %v\n", err)
|
||||
}
|
||||
|
||||
log.Println("Scheduler cleanup complete")
|
||||
}
|
||||
|
||||
// run is the worker's main loop. It drains the queue eagerly and waits for a
|
||||
// workAvail signal when idle, rather than sleeping on a fixed timer.
|
||||
func (w *worker) run(wg *sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
|
||||
log.Printf("Worker %d started\n", w.id)
|
||||
|
||||
for {
|
||||
// Drain: try to grab work before blocking.
|
||||
if item := w.scheduler.queue.Pop(); item != nil {
|
||||
w.executeCheck(item)
|
||||
continue
|
||||
}
|
||||
|
||||
// Queue is empty; wait for new work or a stop signal.
|
||||
select {
|
||||
case <-w.scheduler.workAvail:
|
||||
// Loop back to attempt a Pop.
|
||||
case <-w.scheduler.stopWorkers:
|
||||
log.Printf("Worker %d stopped\n", w.id)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// executeCheck runs a checker and stores the result
|
||||
func (w *worker) executeCheck(item *queueItem) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), CheckExecutionTimeout)
|
||||
defer cancel()
|
||||
|
||||
execution := item.execution
|
||||
schedule := item.schedule
|
||||
|
||||
// Mark execution as running
|
||||
execution.Status = happydns.CheckExecutionRunning
|
||||
if err := w.scheduler.resultUsecase.UpdateCheckExecution(execution); err != nil {
|
||||
log.Printf("Worker %d: Error updating execution status: %v\n", w.id, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Track active execution
|
||||
w.scheduler.mu.Lock()
|
||||
w.scheduler.activeExecutions[execution.Id.String()] = &activeExecution{
|
||||
execution: execution,
|
||||
cancel: cancel,
|
||||
startTime: time.Now(),
|
||||
}
|
||||
w.scheduler.mu.Unlock()
|
||||
|
||||
defer func() {
|
||||
w.scheduler.mu.Lock()
|
||||
delete(w.scheduler.activeExecutions, execution.Id.String())
|
||||
w.scheduler.mu.Unlock()
|
||||
}()
|
||||
|
||||
// Get the checker
|
||||
checker, err := w.scheduler.checkerUsecase.GetChecker(schedule.CheckerName)
|
||||
if err != nil {
|
||||
errMsg := fmt.Sprintf("checker not found: %s - %v", schedule.CheckerName, err)
|
||||
log.Printf("Worker %d: %s\n", w.id, errMsg)
|
||||
_ = w.scheduler.resultUsecase.FailCheckExecution(execution.Id, errMsg)
|
||||
return
|
||||
}
|
||||
|
||||
// Prepare metadata
|
||||
meta := make(map[string]string)
|
||||
meta["target_type"] = schedule.TargetType.String()
|
||||
meta["target_id"] = schedule.TargetId.String()
|
||||
|
||||
// Run the test
|
||||
startTime := time.Now()
|
||||
resultChan := make(chan *happydns.CheckResult, 1)
|
||||
errorChan := make(chan error, 1)
|
||||
|
||||
go func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
errorChan <- fmt.Errorf("checker panicked: %v", r)
|
||||
}
|
||||
}()
|
||||
result, err := checker.RunCheck(schedule.Options, meta)
|
||||
if err != nil {
|
||||
errorChan <- err
|
||||
} else {
|
||||
resultChan <- result
|
||||
}
|
||||
}()
|
||||
|
||||
// Wait for result or timeout
|
||||
var checkResult *happydns.CheckResult
|
||||
var testErr error
|
||||
|
||||
select {
|
||||
case checkResult = <-resultChan:
|
||||
// Check completed successfully
|
||||
case testErr = <-errorChan:
|
||||
// Check returned an error
|
||||
case <-ctx.Done():
|
||||
// Timeout
|
||||
testErr = fmt.Errorf("test execution timeout after %v", CheckExecutionTimeout)
|
||||
}
|
||||
|
||||
duration := time.Since(startTime)
|
||||
|
||||
// Store the result
|
||||
result := &happydns.CheckResult{
|
||||
CheckerName: schedule.CheckerName,
|
||||
CheckType: schedule.TargetType,
|
||||
TargetId: schedule.TargetId,
|
||||
OwnerId: schedule.OwnerId,
|
||||
ExecutedAt: time.Now(),
|
||||
ScheduledCheck: item.execution.ScheduleId != nil,
|
||||
Options: schedule.Options,
|
||||
Duration: duration,
|
||||
}
|
||||
|
||||
if testErr != nil {
|
||||
result.Status = happydns.CheckResultStatusKO
|
||||
result.StatusLine = "Check execution failed"
|
||||
result.Error = testErr.Error()
|
||||
} else if checkResult != nil {
|
||||
result.Status = checkResult.Status
|
||||
result.StatusLine = checkResult.StatusLine
|
||||
result.Report = checkResult.Report
|
||||
} else {
|
||||
result.Status = happydns.CheckResultStatusKO
|
||||
result.StatusLine = "Unknown error"
|
||||
result.Error = "No result or error returned from check"
|
||||
}
|
||||
|
||||
// Save the result
|
||||
if err := w.scheduler.resultUsecase.CreateCheckResult(result); err != nil {
|
||||
log.Printf("Worker %d: Error saving test result: %v\n", w.id, err)
|
||||
_ = w.scheduler.resultUsecase.FailCheckExecution(execution.Id, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Complete the execution
|
||||
if err := w.scheduler.resultUsecase.CompleteCheckExecution(execution.Id, result.Id); err != nil {
|
||||
log.Printf("Worker %d: Error completing execution: %v\n", w.id, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Update schedule if this was a scheduled test
|
||||
if item.execution.ScheduleId != nil {
|
||||
if err := w.scheduler.scheduleUsecase.UpdateScheduleAfterRun(*item.execution.ScheduleId); err != nil {
|
||||
log.Printf("Worker %d: Error updating schedule: %v\n", w.id, err)
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("Worker %d: Completed test %s for target %s (status: %s, duration: %v)\n",
|
||||
w.id, schedule.CheckerName, schedule.TargetId.String(), result.Status, duration)
|
||||
}
|
||||
|
|
@ -55,6 +55,7 @@ func ConsolidateConfig() (opts *happydns.Options, err error) {
|
|||
StorageEngine: "leveldb",
|
||||
MaxResultsPerCheck: 100,
|
||||
ResultRetentionDays: 90,
|
||||
TestWorkers: 2,
|
||||
}
|
||||
|
||||
declareFlags(opts)
|
||||
|
|
|
|||
286
internal/usecase/checkresult/checkschedule_usecase.go
Normal file
286
internal/usecase/checkresult/checkschedule_usecase.go
Normal file
|
|
@ -0,0 +1,286 @@
|
|||
// This file is part of the happyDomain (R) project.
|
||||
// Copyright (c) 2020-2026 happyDomain
|
||||
// Authors: Pierre-Olivier Mercier, et al.
|
||||
//
|
||||
// This program is offered under a commercial and under the AGPL license.
|
||||
// For commercial licensing, contact us at <contact@happydomain.org>.
|
||||
//
|
||||
// For AGPL licensing:
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package checkresult
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"git.happydns.org/happyDomain/model"
|
||||
)
|
||||
|
||||
const (
|
||||
// Default check intervals
|
||||
DefaultUserCheckInterval = 4 * time.Hour // 4 hours for user checks
|
||||
DefaultDomainCheckInterval = 24 * time.Hour // 24 hours for domain checks
|
||||
DefaultServiceCheckInterval = 1 * time.Hour // 1 hour for service checks
|
||||
MinimumCheckInterval = 5 * time.Minute // Minimum interval allowed
|
||||
)
|
||||
|
||||
// CheckScheduleUsecase implements business logic for check schedules
|
||||
type CheckScheduleUsecase struct {
|
||||
storage CheckResultStorage
|
||||
options *happydns.Options
|
||||
}
|
||||
|
||||
// NewCheckScheduleUsecase creates a new check schedule usecase
|
||||
func NewCheckScheduleUsecase(storage CheckResultStorage, options *happydns.Options) *CheckScheduleUsecase {
|
||||
return &CheckScheduleUsecase{
|
||||
storage: storage,
|
||||
options: options,
|
||||
}
|
||||
}
|
||||
|
||||
// ListUserSchedules retrieves all schedules for a specific user
|
||||
func (u *CheckScheduleUsecase) ListUserSchedules(userId happydns.Identifier) ([]*happydns.CheckerSchedule, error) {
|
||||
return u.storage.ListCheckerSchedulesByUser(userId)
|
||||
}
|
||||
|
||||
// ListSchedulesByTarget retrieves all schedules for a specific target
|
||||
func (u *CheckScheduleUsecase) ListSchedulesByTarget(targetType happydns.CheckScopeType, targetId happydns.Identifier) ([]*happydns.CheckerSchedule, error) {
|
||||
return u.storage.ListCheckerSchedulesByTarget(targetType, targetId)
|
||||
}
|
||||
|
||||
// GetSchedule retrieves a specific schedule by ID
|
||||
func (u *CheckScheduleUsecase) GetSchedule(scheduleId happydns.Identifier) (*happydns.CheckerSchedule, error) {
|
||||
return u.storage.GetCheckerSchedule(scheduleId)
|
||||
}
|
||||
|
||||
// CreateSchedule creates a new check schedule with validation
|
||||
func (u *CheckScheduleUsecase) CreateSchedule(schedule *happydns.CheckerSchedule) error {
|
||||
// Validate interval
|
||||
if schedule.Interval < MinimumCheckInterval {
|
||||
return fmt.Errorf("check interval must be at least %v", MinimumCheckInterval)
|
||||
}
|
||||
|
||||
// Set default interval if not specified
|
||||
if schedule.Interval == 0 {
|
||||
schedule.Interval = u.getDefaultInterval(schedule.TargetType)
|
||||
}
|
||||
|
||||
// Calculate next run time
|
||||
if schedule.NextRun.IsZero() {
|
||||
schedule.NextRun = time.Now().Add(schedule.Interval)
|
||||
}
|
||||
|
||||
// Enable by default if not specified
|
||||
if !schedule.Enabled {
|
||||
schedule.Enabled = true
|
||||
}
|
||||
|
||||
return u.storage.CreateCheckerSchedule(schedule)
|
||||
}
|
||||
|
||||
// UpdateSchedule updates an existing schedule
|
||||
func (u *CheckScheduleUsecase) UpdateSchedule(schedule *happydns.CheckerSchedule) error {
|
||||
// Validate interval
|
||||
if schedule.Interval < MinimumCheckInterval {
|
||||
return fmt.Errorf("check interval must be at least %v", MinimumCheckInterval)
|
||||
}
|
||||
|
||||
// Get existing schedule to preserve certain fields
|
||||
existing, err := u.storage.GetCheckerSchedule(schedule.Id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Preserve LastRun if not explicitly changed
|
||||
if schedule.LastRun == nil {
|
||||
schedule.LastRun = existing.LastRun
|
||||
}
|
||||
|
||||
// Recalculate next run time if interval changed
|
||||
if schedule.Interval != existing.Interval {
|
||||
if schedule.LastRun != nil {
|
||||
schedule.NextRun = schedule.LastRun.Add(schedule.Interval)
|
||||
} else {
|
||||
schedule.NextRun = time.Now().Add(schedule.Interval)
|
||||
}
|
||||
}
|
||||
|
||||
return u.storage.UpdateCheckerSchedule(schedule)
|
||||
}
|
||||
|
||||
// DeleteSchedule removes a schedule
|
||||
func (u *CheckScheduleUsecase) DeleteSchedule(scheduleId happydns.Identifier) error {
|
||||
return u.storage.DeleteCheckerSchedule(scheduleId)
|
||||
}
|
||||
|
||||
// EnableSchedule enables a schedule
|
||||
func (u *CheckScheduleUsecase) EnableSchedule(scheduleId happydns.Identifier) error {
|
||||
schedule, err := u.storage.GetCheckerSchedule(scheduleId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
schedule.Enabled = true
|
||||
|
||||
// Reset next run time if it's in the past
|
||||
if schedule.NextRun.Before(time.Now()) {
|
||||
schedule.NextRun = time.Now().Add(schedule.Interval)
|
||||
}
|
||||
|
||||
return u.storage.UpdateCheckerSchedule(schedule)
|
||||
}
|
||||
|
||||
// DisableSchedule disables a schedule
|
||||
func (u *CheckScheduleUsecase) DisableSchedule(scheduleId happydns.Identifier) error {
|
||||
schedule, err := u.storage.GetCheckerSchedule(scheduleId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
schedule.Enabled = false
|
||||
return u.storage.UpdateCheckerSchedule(schedule)
|
||||
}
|
||||
|
||||
// UpdateScheduleAfterRun updates a schedule after it has been executed
|
||||
func (u *CheckScheduleUsecase) UpdateScheduleAfterRun(scheduleId happydns.Identifier) error {
|
||||
schedule, err := u.storage.GetCheckerSchedule(scheduleId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
schedule.LastRun = &now
|
||||
schedule.NextRun = now.Add(schedule.Interval)
|
||||
|
||||
return u.storage.UpdateCheckerSchedule(schedule)
|
||||
}
|
||||
|
||||
// ListDueSchedules retrieves all enabled schedules that are due to run
|
||||
func (u *CheckScheduleUsecase) ListDueSchedules() ([]*happydns.CheckerSchedule, error) {
|
||||
schedules, err := u.storage.ListEnabledCheckerSchedules()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
var dueSchedules []*happydns.CheckerSchedule
|
||||
|
||||
for _, schedule := range schedules {
|
||||
if schedule.Enabled && schedule.NextRun.Before(now) {
|
||||
dueSchedules = append(dueSchedules, schedule)
|
||||
}
|
||||
}
|
||||
|
||||
return dueSchedules, nil
|
||||
}
|
||||
|
||||
// getDefaultInterval returns the default check interval based on target type
|
||||
func (u *CheckScheduleUsecase) getDefaultInterval(targetType happydns.CheckScopeType) time.Duration {
|
||||
switch targetType {
|
||||
case happydns.CheckScopeUser:
|
||||
return DefaultUserCheckInterval
|
||||
case happydns.CheckScopeDomain:
|
||||
return DefaultDomainCheckInterval
|
||||
case happydns.CheckScopeService:
|
||||
return DefaultServiceCheckInterval
|
||||
default:
|
||||
return DefaultDomainCheckInterval
|
||||
}
|
||||
}
|
||||
|
||||
// MergePluginOptions merges plugin options from different scopes
|
||||
// Priority: schedule options > domain options > user options > global options
|
||||
func (u *CheckScheduleUsecase) MergeCheckOptions(
|
||||
globalOpts happydns.CheckerOptions,
|
||||
userOpts happydns.CheckerOptions,
|
||||
domainOpts happydns.CheckerOptions,
|
||||
scheduleOpts happydns.CheckerOptions,
|
||||
) happydns.CheckerOptions {
|
||||
merged := make(happydns.CheckerOptions)
|
||||
|
||||
// Start with global options
|
||||
for k, v := range globalOpts {
|
||||
merged[k] = v
|
||||
}
|
||||
|
||||
// Override with user options
|
||||
for k, v := range userOpts {
|
||||
merged[k] = v
|
||||
}
|
||||
|
||||
// Override with domain options
|
||||
for k, v := range domainOpts {
|
||||
merged[k] = v
|
||||
}
|
||||
|
||||
// Override with schedule options (highest priority)
|
||||
for k, v := range scheduleOpts {
|
||||
merged[k] = v
|
||||
}
|
||||
|
||||
return merged
|
||||
}
|
||||
|
||||
// ValidateScheduleOwnership checks if a user owns a schedule
|
||||
func (u *CheckScheduleUsecase) ValidateScheduleOwnership(scheduleId happydns.Identifier, userId happydns.Identifier) error {
|
||||
schedule, err := u.storage.GetCheckerSchedule(scheduleId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !schedule.OwnerId.Equals(userId) {
|
||||
return fmt.Errorf("user does not own this schedule")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateDefaultSchedulesForTarget creates default schedules for a new target
|
||||
func (u *CheckScheduleUsecase) CreateDefaultSchedulesForTarget(
|
||||
checkerName string,
|
||||
targetType happydns.CheckScopeType,
|
||||
targetId happydns.Identifier,
|
||||
ownerId happydns.Identifier,
|
||||
enabled bool,
|
||||
) error {
|
||||
schedule := &happydns.CheckerSchedule{
|
||||
CheckerName: checkerName,
|
||||
OwnerId: ownerId,
|
||||
TargetType: targetType,
|
||||
TargetId: targetId,
|
||||
Interval: u.getDefaultInterval(targetType),
|
||||
Enabled: enabled,
|
||||
NextRun: time.Now().Add(u.getDefaultInterval(targetType)),
|
||||
Options: make(happydns.CheckerOptions),
|
||||
}
|
||||
|
||||
return u.CreateSchedule(schedule)
|
||||
}
|
||||
|
||||
// DeleteSchedulesForTarget removes all schedules for a target
|
||||
func (u *CheckScheduleUsecase) DeleteSchedulesForTarget(targetType happydns.CheckScopeType, targetId happydns.Identifier) error {
|
||||
schedules, err := u.storage.ListCheckerSchedulesByTarget(targetType, targetId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, schedule := range schedules {
|
||||
if err := u.storage.DeleteCheckerSchedule(schedule.Id); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -27,6 +27,8 @@ import (
|
|||
|
||||
// SchedulerUsecase defines the interface for triggering on-demand checks
|
||||
type SchedulerUsecase interface {
|
||||
Run()
|
||||
Close()
|
||||
TriggerOnDemandCheck(checkerName string, targetType CheckScopeType, targetID Identifier, userID Identifier, options CheckerOptions) (Identifier, error)
|
||||
}
|
||||
|
||||
|
|
@ -63,6 +65,30 @@ type CheckerSchedule struct {
|
|||
Options CheckerOptions `json:"options,omitempty"`
|
||||
}
|
||||
|
||||
// SchedulerStatus holds a snapshot of the scheduler state for monitoring
|
||||
type SchedulerStatus struct {
|
||||
// ConfigEnabled indicates if the scheduler is enabled in the configuration file
|
||||
ConfigEnabled bool `json:"config_enabled"`
|
||||
|
||||
// RuntimeEnabled indicates if the scheduler is currently enabled at runtime
|
||||
RuntimeEnabled bool `json:"runtime_enabled"`
|
||||
|
||||
// Running indicates if the scheduler goroutine is currently running
|
||||
Running bool `json:"running"`
|
||||
|
||||
// WorkerCount is the number of worker goroutines
|
||||
WorkerCount int `json:"worker_count"`
|
||||
|
||||
// QueueSize is the number of items currently waiting in the execution queue
|
||||
QueueSize int `json:"queue_size"`
|
||||
|
||||
// ActiveCount is the number of checks currently being executed
|
||||
ActiveCount int `json:"active_count"`
|
||||
|
||||
// NextSchedules contains the upcoming scheduled checks sorted by next run time
|
||||
NextSchedules []*CheckerSchedule `json:"next_schedules"`
|
||||
}
|
||||
|
||||
// CheckerScheduleUsecase defines business logic for check schedules
|
||||
type CheckerScheduleUsecase interface {
|
||||
// ListUserSchedules retrieves all schedules for a specific user
|
||||
|
|
|
|||
|
|
@ -107,6 +107,12 @@ type Options struct {
|
|||
|
||||
// ResultRetentionDays is how long to keep test results before cleanup
|
||||
ResultRetentionDays int
|
||||
|
||||
// TestWorkers is the number of concurrent test executions allowed
|
||||
TestWorkers int
|
||||
|
||||
// DisableScheduler disables the background test scheduler
|
||||
DisableScheduler bool
|
||||
}
|
||||
|
||||
// GetBaseURL returns the full url to the absolute ExternalURL, including BaseURL.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue