Files
EvoBGP/internal/scheduler/run.go
T

142 lines
3.8 KiB
Go

// Package scheduler drives module refresh intervals and enqueues module_refresh jobs on the shared Registry.
package scheduler
import (
"context"
"fmt"
"io"
"log"
"net/http"
"strings"
"time"
"evobgp/internal/broker"
"evobgp/internal/config"
"evobgp/internal/jobs"
"evobgp/internal/store"
)
// Deps wires the scheduler to the store. Use either Jobs (same process as API, e.g. evobgp-all) or
// APIBase+APIToken to call the control plane over HTTP (separate containers in reference Compose).
type Deps struct {
Store store.Backend
Jobs *jobs.Registry
APIBase string // e.g. http://evobgp-api:8080
APIToken string // Bearer token (operator/editor)
HTTP *http.Client
}
// Run blocks until ctx is cancelled. When deps is nil or incomplete, falls back to connect-only stub logging.
func Run(ctx context.Context, deps *Deps) {
cfg := config.Load()
broker.LogConnect(ctx, cfg.BrokerURL)
if deps == nil || deps.Store == nil {
runStub(ctx)
return
}
if deps.Jobs == nil && (strings.TrimSpace(deps.APIBase) == "" || strings.TrimSpace(deps.APIToken) == "") {
runStub(ctx)
return
}
t := time.NewTicker(30 * time.Second)
defer t.Stop()
if deps.Jobs != nil {
log.Printf("evobgp-scheduler: active (in-process enqueue module_refresh)")
} else {
log.Printf("evobgp-scheduler: active (HTTP POST .../modules/{id}/refresh → %s)", strings.TrimSpace(deps.APIBase))
}
for {
select {
case <-ctx.Done():
log.Printf("evobgp-scheduler: stopped")
return
case <-t.C:
tick(context.Background(), deps)
}
}
}
func tick(ctx context.Context, deps *Deps) {
tenants, err := deps.Store.ListTenantIDs()
if err != nil {
log.Printf("evobgp-scheduler: list tenants: %v", err)
return
}
for _, tid := range tenants {
for _, mod := range deps.Store.ListModules(tid) {
if !mod.Enabled || mod.Type == "IP_RANGES" {
continue
}
interval := mod.RefreshIntervalSec
if interval <= 0 {
continue
}
win := interval
if win < 60 {
win = 60
}
bucket := time.Now().Unix() / int64(win)
key := fmt.Sprintf("sched-%s-%d", mod.ID, bucket)
if deps.Jobs != nil {
mid := mod.ID
_, created, err := deps.Jobs.Enqueue(tid, jobs.KindModuleRefresh, &key, &mid, map[string]any{
"module_id": mod.ID,
"trigger": "scheduler",
})
if err != nil {
log.Printf("evobgp-scheduler: enqueue module %s: %v", mod.ID, err)
continue
}
if created {
log.Printf("evobgp-scheduler: queued refresh for module %s (%s)", mod.ID, mod.Type)
}
continue
}
if err := postModuleRefresh(ctx, deps, mod.ID, key); err != nil {
log.Printf("evobgp-scheduler: http refresh module %s: %v", mod.ID, err)
}
}
}
}
func postModuleRefresh(ctx context.Context, deps *Deps, moduleID, idempotencyKey string) error {
base := strings.TrimRight(strings.TrimSpace(deps.APIBase), "/")
u := base + "/v1/modules/" + moduleID + "/refresh"
req, err := http.NewRequestWithContext(ctx, http.MethodPost, u, nil)
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+strings.TrimSpace(deps.APIToken))
if idempotencyKey != "" {
req.Header.Set("Idempotency-Key", idempotencyKey)
}
hc := deps.HTTP
if hc == nil {
hc = http.DefaultClient
}
resp, err := hc.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusNoContent || resp.StatusCode == http.StatusAccepted {
return nil
}
b, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
return fmt.Errorf("%s: %s", resp.Status, strings.TrimSpace(string(b)))
}
func runStub(ctx context.Context) {
t := time.NewTicker(60 * time.Second)
defer t.Stop()
log.Printf("evobgp-scheduler: idle stub (no store/jobs: pass scheduler.Deps from evobgp-all or bootstrap worker)")
for {
select {
case <-ctx.Done():
log.Printf("evobgp-scheduler: stopped")
return
case <-t.C:
}
}
}