fix: Restore all files lost during destructive rebase
A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.
This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).
Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
222
edu-search-service/internal/scheduler/scheduler.go
Normal file
222
edu-search-service/internal/scheduler/scheduler.go
Normal file
@@ -0,0 +1,222 @@
|
||||
package scheduler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// CrawlFunc is the function signature for executing a crawl
|
||||
type CrawlFunc func(ctx context.Context) error
|
||||
|
||||
// Status represents the current scheduler status
|
||||
type Status struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Running bool `json:"running"`
|
||||
LastRun time.Time `json:"last_run,omitempty"`
|
||||
LastRunStatus string `json:"last_run_status,omitempty"`
|
||||
NextRun time.Time `json:"next_run,omitempty"`
|
||||
Interval string `json:"interval"`
|
||||
}
|
||||
|
||||
// Scheduler handles automatic crawl scheduling
|
||||
type Scheduler struct {
|
||||
mu sync.RWMutex
|
||||
enabled bool
|
||||
interval time.Duration
|
||||
crawlFunc CrawlFunc
|
||||
running bool
|
||||
lastRun time.Time
|
||||
lastRunStatus string
|
||||
stopChan chan struct{}
|
||||
doneChan chan struct{}
|
||||
}
|
||||
|
||||
// Config holds scheduler configuration
|
||||
type Config struct {
|
||||
Enabled bool
|
||||
Interval time.Duration
|
||||
}
|
||||
|
||||
// NewScheduler creates a new crawler scheduler
|
||||
func NewScheduler(cfg Config, crawlFunc CrawlFunc) *Scheduler {
|
||||
return &Scheduler{
|
||||
enabled: cfg.Enabled,
|
||||
interval: cfg.Interval,
|
||||
crawlFunc: crawlFunc,
|
||||
stopChan: make(chan struct{}),
|
||||
doneChan: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
// Start begins the scheduler loop
|
||||
func (s *Scheduler) Start() {
|
||||
if !s.enabled {
|
||||
log.Println("Scheduler is disabled")
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("Scheduler starting with interval: %v", s.interval)
|
||||
|
||||
go s.run()
|
||||
}
|
||||
|
||||
// Stop gracefully stops the scheduler
|
||||
func (s *Scheduler) Stop() {
|
||||
s.mu.Lock()
|
||||
if !s.enabled {
|
||||
s.mu.Unlock()
|
||||
return
|
||||
}
|
||||
s.mu.Unlock()
|
||||
|
||||
close(s.stopChan)
|
||||
<-s.doneChan
|
||||
log.Println("Scheduler stopped")
|
||||
}
|
||||
|
||||
// run is the main scheduler loop
|
||||
func (s *Scheduler) run() {
|
||||
defer close(s.doneChan)
|
||||
|
||||
// Calculate time until first run
|
||||
// Default: run at 2:00 AM to minimize impact
|
||||
now := time.Now()
|
||||
nextRun := s.calculateNextRun(now)
|
||||
|
||||
log.Printf("Scheduler: first crawl scheduled for %v", nextRun)
|
||||
|
||||
timer := time.NewTimer(time.Until(nextRun))
|
||||
defer timer.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-s.stopChan:
|
||||
return
|
||||
case <-timer.C:
|
||||
s.executeCrawl()
|
||||
// Schedule next run
|
||||
nextRun = time.Now().Add(s.interval)
|
||||
timer.Reset(s.interval)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// calculateNextRun determines when the next crawl should occur
|
||||
func (s *Scheduler) calculateNextRun(from time.Time) time.Time {
|
||||
// If interval is 24h or more, schedule for 2:00 AM
|
||||
if s.interval >= 24*time.Hour {
|
||||
next := time.Date(from.Year(), from.Month(), from.Day(), 2, 0, 0, 0, from.Location())
|
||||
if next.Before(from) || next.Equal(from) {
|
||||
next = next.Add(24 * time.Hour)
|
||||
}
|
||||
return next
|
||||
}
|
||||
|
||||
// For shorter intervals, start immediately
|
||||
return from.Add(1 * time.Minute)
|
||||
}
|
||||
|
||||
// executeCrawl runs the crawl function
|
||||
func (s *Scheduler) executeCrawl() {
|
||||
s.mu.Lock()
|
||||
if s.running {
|
||||
s.mu.Unlock()
|
||||
log.Println("Scheduler: crawl already running, skipping")
|
||||
return
|
||||
}
|
||||
s.running = true
|
||||
s.mu.Unlock()
|
||||
|
||||
log.Println("Scheduler: starting scheduled crawl")
|
||||
startTime := time.Now()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 4*time.Hour)
|
||||
defer cancel()
|
||||
|
||||
err := s.crawlFunc(ctx)
|
||||
|
||||
s.mu.Lock()
|
||||
s.running = false
|
||||
s.lastRun = startTime
|
||||
if err != nil {
|
||||
s.lastRunStatus = "failed: " + err.Error()
|
||||
log.Printf("Scheduler: crawl failed after %v: %v", time.Since(startTime), err)
|
||||
} else {
|
||||
s.lastRunStatus = "success"
|
||||
log.Printf("Scheduler: crawl completed successfully in %v", time.Since(startTime))
|
||||
}
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
// TriggerCrawl manually triggers a crawl
|
||||
func (s *Scheduler) TriggerCrawl() error {
|
||||
s.mu.Lock()
|
||||
if s.running {
|
||||
s.mu.Unlock()
|
||||
return ErrCrawlAlreadyRunning
|
||||
}
|
||||
s.running = true
|
||||
s.mu.Unlock()
|
||||
|
||||
log.Println("Scheduler: manual crawl triggered")
|
||||
|
||||
go func() {
|
||||
startTime := time.Now()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 4*time.Hour)
|
||||
defer cancel()
|
||||
|
||||
err := s.crawlFunc(ctx)
|
||||
|
||||
s.mu.Lock()
|
||||
s.running = false
|
||||
s.lastRun = startTime
|
||||
if err != nil {
|
||||
s.lastRunStatus = "failed: " + err.Error()
|
||||
log.Printf("Scheduler: manual crawl failed after %v: %v", time.Since(startTime), err)
|
||||
} else {
|
||||
s.lastRunStatus = "success"
|
||||
log.Printf("Scheduler: manual crawl completed successfully in %v", time.Since(startTime))
|
||||
}
|
||||
s.mu.Unlock()
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Status returns the current scheduler status
|
||||
func (s *Scheduler) Status() Status {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
status := Status{
|
||||
Enabled: s.enabled,
|
||||
Running: s.running,
|
||||
LastRun: s.lastRun,
|
||||
LastRunStatus: s.lastRunStatus,
|
||||
Interval: s.interval.String(),
|
||||
}
|
||||
|
||||
if s.enabled && !s.lastRun.IsZero() {
|
||||
status.NextRun = s.lastRun.Add(s.interval)
|
||||
}
|
||||
|
||||
return status
|
||||
}
|
||||
|
||||
// IsRunning returns true if a crawl is currently in progress
|
||||
func (s *Scheduler) IsRunning() bool {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
return s.running
|
||||
}
|
||||
|
||||
// Errors
|
||||
type SchedulerError string
|
||||
|
||||
func (e SchedulerError) Error() string { return string(e) }
|
||||
|
||||
const (
|
||||
ErrCrawlAlreadyRunning = SchedulerError("crawl already running")
|
||||
)
|
||||
Reference in New Issue
Block a user