feat: downloader

This commit is contained in:
Anatoly Antonov 2025-06-23 04:19:26 +03:00
parent b9c1a98895
commit 42e7924be9
37 changed files with 2422 additions and 94 deletions

21
pkg/redis/interface.go Normal file
View file

@ -0,0 +1,21 @@
package redis
import (
"context"
"time"
)
// Service defines the interface for Redis operations
type Service interface {
// Lock acquires a distributed lock
Lock(ctx context.Context, key string, ttl time.Duration) (func(context.Context), error)
// Set sets a key with value and TTL
Set(key string, value []byte, ttl time.Duration) error
// Get retrieves a value by key
Get(key string) ([]byte, error)
// Close closes the Redis connection
Close() error
}

89
pkg/redis/redis.go Normal file
View file

@ -0,0 +1,89 @@
package redis
import (
"context"
"fmt"
"time"
"git.intra.yksa.space/gsn/predictor/internal/pkg/errcodes"
"github.com/bsm/redislock"
"github.com/redis/go-redis/v9"
)
type Client struct {
client *redis.Client
locker *redislock.Client
}
// Ensure Client implements Service interface
var _ Service = (*Client)(nil)
type Config struct {
Host string
Port int
Password string
DB int
}
func New(cfg Config) (*Client, error) {
client := redis.NewClient(&redis.Options{
Addr: fmt.Sprintf("%s:%d", cfg.Host, cfg.Port),
Password: cfg.Password,
DB: cfg.DB,
})
// Test connection
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := client.Ping(ctx).Err(); err != nil {
return nil, fmt.Errorf("failed to connect to redis: %w", err)
}
locker := redislock.New(client)
return &Client{
client: client,
locker: locker,
}, nil
}
func (c *Client) Lock(ctx context.Context, key string, ttl time.Duration) (func(context.Context), error) {
lock, err := c.locker.Obtain(ctx, key, ttl, nil)
if err != nil {
if err == redislock.ErrNotObtained {
return nil, errcodes.ErrRedisLockAlreadyLocked
}
return nil, errcodes.Wrap(err, "failed to obtain redis lock")
}
unlock := func(ctx context.Context) {
lock.Release(ctx)
}
return unlock, nil
}
func (c *Client) Set(key string, value []byte, ttl time.Duration) error {
ctx := context.Background()
if err := c.client.Set(ctx, key, value, ttl).Err(); err != nil {
return errcodes.Wrap(err, "failed to set redis key")
}
return nil
}
func (c *Client) Get(key string) ([]byte, error) {
ctx := context.Background()
result := c.client.Get(ctx, key)
if result.Err() != nil {
if result.Err() == redis.Nil {
return nil, errcodes.ErrRedisCacheMiss
}
return nil, errcodes.Wrap(result.Err(), "failed to get redis key")
}
return result.Bytes()
}
func (c *Client) Close() error {
return c.client.Close()
}

5
pkg/scheduler/config.go Normal file
View file

@ -0,0 +1,5 @@
package scheduler
type Config struct {
Enabled bool `env:"ENABLED" envDefault:"true"`
}

View file

@ -0,0 +1,99 @@
package scheduler
import (
"context"
"time"
"git.intra.yksa.space/gsn/predictor/internal/pkg/errcodes"
"github.com/go-co-op/gocron"
"go.uber.org/zap"
)
type Job interface {
GetInterval() time.Duration
GetTimeout() time.Duration
GetCount() int
GetAsync() bool
Execute(context.Context) error
}
type Scheduler struct {
scheduler *gocron.Scheduler
logger *zap.Logger
}
func New(logger *zap.Logger) *Scheduler {
scheduler := gocron.NewScheduler(time.UTC)
return &Scheduler{
scheduler: scheduler,
logger: logger,
}
}
func (s *Scheduler) AddJob(job Job) error {
interval := job.GetInterval()
timeout := job.GetTimeout()
count := job.GetCount()
async := job.GetAsync()
// Validate job parameters
if !async && count != 1 {
return errcodes.ErrSchedulerInvalidJob
}
if timeout > interval {
return errcodes.ErrSchedulerTimeoutTooLong
}
// Create job function with timeout
jobFunc := func() {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
if err := job.Execute(ctx); err != nil {
s.logger.Error("job execution failed",
zap.Error(err),
zap.Duration("interval", interval),
zap.Duration("timeout", timeout))
} else {
s.logger.Debug("job executed successfully",
zap.Duration("interval", interval),
zap.Duration("timeout", timeout))
}
}
// Add job to scheduler
schedulerJob := s.scheduler.Every(interval)
if !async {
schedulerJob = schedulerJob.SingletonMode()
}
if count > 0 {
schedulerJob = schedulerJob.LimitRunsTo(count)
}
schedulerJob.Do(jobFunc)
s.logger.Info("job added to scheduler",
zap.Duration("interval", interval),
zap.Duration("timeout", timeout),
zap.Int("count", count),
zap.Bool("async", async))
return nil
}
func (s *Scheduler) Start() {
s.scheduler.StartAsync()
s.logger.Info("scheduler started")
}
func (s *Scheduler) Stop() {
s.scheduler.Stop()
s.logger.Info("scheduler stopped")
}
func (s *Scheduler) IsRunning() bool {
return s.scheduler.IsRunning()
}