fix: deep bug hunt II — concurrency, security & optimization (25 files)
Critical: watchdog mutex panic safety, SetGeoAppOverride nil guard, SSD-only app DB restore fallback. High: double deploy race (atomic Deploying flag), delete/remove during deploy guard, ScanStacks overwrite protection, FileBrowser mount mutex, PushEvent history, PushOnce error handling, DB dump sync+close before rename, restic retry fresh context, encrypt failure logging, cross-backup path traversal validation, deepCopyStack completeness. Security: constant-time API key comparison, login rate limiting (5/min), git credential masking in logs, storage path prefix traversal fix. Concurrency: MigrateEncryption lock ordering, SubdomainInUse I/O outside lock, scheduler late-registered jobs, SQLite WAL verification, metrics shutdown context, telemetry scan error logging, asset sync lock scope. Optimization: streaming file copy for DB dumps, restic stats dedup, atomic infra config copy. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -41,12 +41,13 @@ type Job struct {
|
||||
|
||||
// Scheduler manages periodic and daily jobs.
|
||||
type Scheduler struct {
|
||||
mu sync.Mutex
|
||||
jobs []*Job
|
||||
logger *log.Logger
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
wg sync.WaitGroup
|
||||
mu sync.Mutex
|
||||
jobs []*Job
|
||||
logger *log.Logger
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
wg sync.WaitGroup
|
||||
started bool
|
||||
}
|
||||
|
||||
// New creates a new Scheduler.
|
||||
@@ -57,6 +58,7 @@ func New(logger *log.Logger) *Scheduler {
|
||||
}
|
||||
|
||||
// Every registers a periodic job that runs every interval.
|
||||
// If the scheduler is already started, the job's goroutine is launched immediately.
|
||||
func (s *Scheduler) Every(name string, interval time.Duration, fn JobFunc) {
|
||||
if interval <= 0 {
|
||||
s.logger.Printf("[ERROR] Periodic job %s has invalid interval %s — job not registered", name, interval)
|
||||
@@ -66,15 +68,22 @@ func (s *Scheduler) Every(name string, interval time.Duration, fn JobFunc) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
s.jobs = append(s.jobs, &Job{
|
||||
job := &Job{
|
||||
Name: name,
|
||||
Fn: fn,
|
||||
Interval: interval,
|
||||
})
|
||||
}
|
||||
s.jobs = append(s.jobs, job)
|
||||
s.logger.Printf("[SCHED] Registered periodic job: %s (every %s)", name, interval)
|
||||
|
||||
if s.started {
|
||||
s.wg.Add(1)
|
||||
go s.runPeriodicJob(job)
|
||||
}
|
||||
}
|
||||
|
||||
// Daily registers a job that runs once per day at the specified time (HH:MM) in Europe/Budapest timezone.
|
||||
// If the scheduler is already started, the job's goroutine is launched immediately.
|
||||
func (s *Scheduler) Daily(name string, timeStr string, fn JobFunc) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
@@ -85,14 +94,20 @@ func (s *Scheduler) Daily(name string, timeStr string, fn JobFunc) {
|
||||
return
|
||||
}
|
||||
|
||||
s.jobs = append(s.jobs, &Job{
|
||||
job := &Job{
|
||||
Name: name,
|
||||
Fn: fn,
|
||||
Schedule: timeStr,
|
||||
})
|
||||
}
|
||||
s.jobs = append(s.jobs, job)
|
||||
|
||||
nextRun := nextDailyRun(timeStr)
|
||||
s.logger.Printf("[SCHED] Daily job %s scheduled for %s", name, nextRun.Format("2006-01-02 15:04 MST"))
|
||||
|
||||
if s.started {
|
||||
s.wg.Add(1)
|
||||
go s.runDailyJob(job)
|
||||
}
|
||||
}
|
||||
|
||||
// Start begins running all registered jobs. Safe to call only once.
|
||||
@@ -104,6 +119,7 @@ func (s *Scheduler) Start(ctx context.Context) {
|
||||
return
|
||||
}
|
||||
s.ctx, s.cancel = context.WithCancel(ctx)
|
||||
s.started = true
|
||||
|
||||
for _, job := range s.jobs {
|
||||
if job.Interval > 0 {
|
||||
|
||||
Reference in New Issue
Block a user