mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-06-14 16:12:00 +00:00
25fd9c9d6d
* feat(cron): configurable default timezone for cron expressions Cron expressions (e.g. "0 8 * * *") are evaluated relative to a timezone. Without an explicit per-job timezone, they default to the server's system timezone, which may not match the user's local time — especially in Docker containers (default UTC) or multi-region deployments. This adds a `default_timezone` setting to `CronConfig` (IANA format, e.g. "Asia/Ho_Chi_Minh") that is applied as fallback when a cron job has no explicit `schedule.tz`. The setting is configurable via the UI config page (Integrations → Cron Scheduler) and hot-reloads on config changes. Backend: - Add `DefaultTimezone` field to `CronConfig` - Add `SetDefaultTimezone()` to `CronStore` interface + PG implementation - Apply default TZ in `AddJob()` when `schedule.TZ` is empty - Wire at startup + subscribe to config change events for hot reload - Update cron tool description so LLM knows about gateway default Frontend: - Add timezone dropdown (20 common IANA timezones) to Cron config section - Add i18n keys for en, vi, zh * fix(cron): apply default timezone to existing jobs via computeNextRun Pass defaultTZ as fallback to computeNextRun so existing cron jobs (with timezone = NULL in DB) also use the gateway's configured default timezone when computing next_run_at. This ensures old jobs benefit from the timezone setting without needing a DB migration or backfill. --------- Co-authored-by: Luvu182 <208665161+Luvu182@users.noreply.github.com>
98 lines
2.2 KiB
Go
98 lines
2.2 KiB
Go
package pg
|
|
|
|
import (
|
|
"database/sql"
|
|
"log/slog"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/nextlevelbuilder/goclaw/internal/cron"
|
|
"github.com/nextlevelbuilder/goclaw/internal/store"
|
|
)
|
|
|
|
const defaultCronCacheTTL = 2 * time.Minute
|
|
|
|
// PGCronStore implements store.CronStore backed by Postgres.
|
|
// GetDueJobs() uses an in-memory cache with TTL to reduce DB polling (1s interval).
|
|
type PGCronStore struct {
|
|
db *sql.DB
|
|
mu sync.Mutex
|
|
onJob func(job *store.CronJob) (*store.CronJobResult, error)
|
|
onEvent func(event store.CronEvent)
|
|
running bool
|
|
stop chan struct{}
|
|
|
|
// Job cache: reduces GetDueJobs polling from 86,400 queries/day to ~720/day
|
|
jobCache []store.CronJob
|
|
cacheLoaded bool
|
|
cacheTime time.Time
|
|
cacheTTL time.Duration
|
|
|
|
retryCfg cron.RetryConfig
|
|
defaultTZ string // fallback IANA timezone for cron jobs without explicit TZ
|
|
}
|
|
|
|
func NewPGCronStore(db *sql.DB) *PGCronStore {
|
|
return &PGCronStore{db: db, cacheTTL: defaultCronCacheTTL, retryCfg: cron.DefaultRetryConfig()}
|
|
}
|
|
|
|
// SetRetryConfig overrides the default retry configuration.
|
|
func (s *PGCronStore) SetRetryConfig(cfg cron.RetryConfig) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
s.retryCfg = cfg
|
|
}
|
|
|
|
// SetDefaultTimezone sets the fallback IANA timezone for cron expressions
|
|
// when a job does not specify its own timezone.
|
|
func (s *PGCronStore) SetDefaultTimezone(tz string) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
s.defaultTZ = tz
|
|
}
|
|
|
|
func (s *PGCronStore) Start() error {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
if s.running {
|
|
return nil
|
|
}
|
|
s.stop = make(chan struct{})
|
|
s.running = true
|
|
s.recomputeStaleJobs()
|
|
go s.runLoop()
|
|
slog.Info("pg cron service started")
|
|
return nil
|
|
}
|
|
|
|
func (s *PGCronStore) Stop() {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
if !s.running {
|
|
return
|
|
}
|
|
close(s.stop)
|
|
s.running = false
|
|
}
|
|
|
|
func (s *PGCronStore) SetOnJob(handler func(job *store.CronJob) (*store.CronJobResult, error)) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
s.onJob = handler
|
|
}
|
|
|
|
func (s *PGCronStore) SetOnEvent(handler func(event store.CronEvent)) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
s.onEvent = handler
|
|
}
|
|
|
|
func (s *PGCronStore) emitEvent(event store.CronEvent) {
|
|
s.mu.Lock()
|
|
fn := s.onEvent
|
|
s.mu.Unlock()
|
|
if fn != nil {
|
|
fn(event)
|
|
}
|
|
}
|