Рыба проекта. Минимальная функциональность

This commit is contained in:
2026-08-03 22:22:24 +03:00
commit 8c8631ac9c
80 changed files with 10618 additions and 0 deletions
+66
View File
@@ -0,0 +1,66 @@
package cron
import (
"context"
"fmt"
"sync"
"github.com/robfig/cron/v3"
"git.tswf.io/infra/go-synapse-backupper/pkg/domain"
)
// cronScheduler wraps robfig/cron/v3 to implement domain.Scheduler.
type cronScheduler struct {
cron *cron.Cron
waitGroup sync.WaitGroup
stopCancel context.CancelFunc
stoppedCtx context.Context
}
// NewCronScheduler creates a new cron-based scheduler.
// The expr must be a valid cron expression. When WithSeconds is used
// (as in this implementation) the expression should contain six fields.
func NewCronScheduler(
expr string,
job func(),
) (
domain.Scheduler,
error,
) {
scheduler := &cronScheduler{
cron: cron.New(cron.WithSeconds()),
}
_, err := scheduler.cron.AddFunc(
expr,
func() {
scheduler.waitGroup.Add(1)
defer scheduler.waitGroup.Done()
job()
},
)
if err != nil {
return nil, fmt.Errorf("invalid cron expression %q: %w", expr, err)
}
scheduler.stoppedCtx, scheduler.stopCancel = context.WithCancel(context.Background())
return scheduler, nil
}
// Start begins executing the scheduled job.
func (s *cronScheduler) Start() {
s.cron.Start()
}
// Stop halts the scheduler and returns a context that is cancelled
// once all in-flight jobs have completed.
func (s *cronScheduler) Stop() context.Context {
s.cron.Stop()
go func() {
s.waitGroup.Wait()
s.stopCancel()
}()
return s.stoppedCtx
}
+61
View File
@@ -0,0 +1,61 @@
package cron
import (
"testing"
"time"
)
func TestJobFiresAtExpectedTime(t *testing.T) {
fired := make(chan struct{}, 1)
job := func() {
select {
case fired <- struct{}{}:
default:
}
}
// Every second (6 fields because cron is created with WithSeconds).
scheduler, err := NewCronScheduler("* * * * * *", job)
if err != nil {
t.Fatalf("NewCronScheduler failed: %v", err)
}
scheduler.Start()
select {
case <-fired:
// Job fired as expected.
case <-time.After(3 * time.Second):
t.Fatal("job did not fire within expected time")
}
stoppedCtx := scheduler.Stop()
select {
case <-stoppedCtx.Done():
// Scheduler stopped cleanly.
case <-time.After(2 * time.Second):
t.Fatal("scheduler did not stop within expected time")
}
}
func TestStopReturnsContext(t *testing.T) {
scheduler, err := NewCronScheduler("* * * * * *", func() {})
if err != nil {
t.Fatalf("NewCronScheduler failed: %v", err)
}
scheduler.Start()
stoppedCtx := scheduler.Stop()
if stoppedCtx == nil {
t.Fatal("Stop returned nil context")
}
select {
case <-stoppedCtx.Done():
// Expected.
case <-time.After(2 * time.Second):
t.Fatal("stopped context was not cancelled")
}
}