init push
This commit is contained in:
83
cmd/worker/file_worker.go
Normal file
83
cmd/worker/file_worker.go
Normal file
@@ -0,0 +1,83 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
queueapp "wucher/internal/app/queueing"
|
||||
appworker "wucher/internal/app/worker"
|
||||
"wucher/internal/config"
|
||||
"wucher/internal/queue"
|
||||
mysqlrepo "wucher/internal/repository/mysql"
|
||||
s3repo "wucher/internal/repository/s3"
|
||||
"wucher/internal/resilience"
|
||||
"wucher/internal/service"
|
||||
"wucher/internal/shared/pkg/logger"
|
||||
)
|
||||
|
||||
func runFileWorker(appCfg config.AppConfig) error {
|
||||
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
||||
defer stop()
|
||||
|
||||
appLogger := logger.New(appCfg.Logging).With("component", "worker", "job", "file_processing")
|
||||
resCfg := appCfg.Resilience
|
||||
fileQueueCfg := appCfg.FileQueue
|
||||
fileSQSCfg := appCfg.FileSQS
|
||||
if err := config.ValidateDistinctQueueURLs(appCfg.SQS, fileSQSCfg); err != nil {
|
||||
return err
|
||||
}
|
||||
if strings.TrimSpace(fileSQSCfg.QueueURL) == "" {
|
||||
return errors.New("FILE_SQS_QUEUE_URL is required for file worker")
|
||||
}
|
||||
db, err := mysqlrepo.Connect(appCfg.MySQL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fileRepo := mysqlrepo.NewFileManagerFileRepository(db)
|
||||
sqsBreaker := resilience.NewExecutor("sqs", resCfg.SQS, appLogger, resilience.ClassifySQSError)
|
||||
consumer, serializer, err := queueapp.NewFileProcessingConsumer(ctx, fileQueueCfg, fileSQSCfg, sqsBreaker)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
idempotency := queueapp.NewIdempotencyStoreWithPrefix(db, fileQueueCfg.IdempotencyKeyPrefix)
|
||||
useCaseDeps := service.FileProcessingUseCaseDependencies{
|
||||
Logger: appLogger,
|
||||
FileStatusEvents: fileRepo,
|
||||
GotenbergURL: appCfg.Gotenberg.URL,
|
||||
}
|
||||
fileStorageCfg := appCfg.FileStorage
|
||||
if fileStorageCfg.Bucket != "" && fileStorageCfg.Region != "" {
|
||||
storage, err := s3repo.NewFileObjectStorage(ctx, fileStorageCfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
useCaseDeps.Storage = storage
|
||||
}
|
||||
useCase := service.NewFileProcessingUseCase(fileRepo, useCaseDeps)
|
||||
handler := queue.NewFileProcessingJobQueueHandler(useCase, service.IsFileProcessingPermanentError)
|
||||
var dispatcher *appworker.FileProcessingOutboxDispatcher
|
||||
if fileQueueCfg.OutboxEnabled {
|
||||
rawProducer, err := queueapp.NewQueueProducer(ctx, fileSQSCfg, sqsBreaker, "outbox_dispatcher")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dispatcher = appworker.NewFileProcessingOutboxDispatcher(fileQueueCfg, fileRepo, rawProducer, appLogger)
|
||||
}
|
||||
runner := appworker.NewFileProcessingRunner(fileQueueCfg, consumer, serializer, handler, idempotency, dispatcher, appLogger)
|
||||
|
||||
appLogger.Info("file worker started",
|
||||
"backend", "sqs",
|
||||
"monitor_addr", fileQueueCfg.WorkerMonitorAddr,
|
||||
"queue_url", fileSQSCfg.QueueURL,
|
||||
"outbox_enabled", fileQueueCfg.OutboxEnabled,
|
||||
)
|
||||
if err := runner.Start(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
appLogger.Info("file worker stopped")
|
||||
return nil
|
||||
}
|
||||
99
cmd/worker/mail_worker.go
Normal file
99
cmd/worker/mail_worker.go
Normal file
@@ -0,0 +1,99 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"log/slog"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
queueapp "wucher/internal/app/queueing"
|
||||
appworker "wucher/internal/app/worker"
|
||||
"wucher/internal/config"
|
||||
"wucher/internal/shared/pkg/logger"
|
||||
mysqlrepo "wucher/internal/repository/mysql"
|
||||
"wucher/internal/resilience"
|
||||
"wucher/internal/service"
|
||||
)
|
||||
|
||||
func runMailWorker(appCfg config.AppConfig) error {
|
||||
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
||||
defer stop()
|
||||
|
||||
appLogger := logger.New(appCfg.Logging).With("component", "worker", "job", "email")
|
||||
resCfg := appCfg.Resilience
|
||||
emailCfg := appCfg.Email
|
||||
|
||||
queueCfg := appCfg.Queue
|
||||
sqsCfg := appCfg.SQS
|
||||
db, err := mysqlrepo.Connect(appCfg.MySQL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
authRepo := mysqlrepo.NewAuthRepository(db)
|
||||
|
||||
sqsBreaker := resilience.NewExecutor("sqs", resCfg.SQS, appLogger, resilience.ClassifySQSError)
|
||||
consumer, serializer, err := queueapp.NewEmailConsumer(ctx, queueCfg, sqsCfg, sqsBreaker)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
idempotency := queueapp.NewIdempotencyStore(db, queueCfg)
|
||||
var dispatcher *appworker.OutboxDispatcher
|
||||
if queueCfg.OutboxEnabled {
|
||||
rawProducer, err := queueapp.NewQueueProducer(ctx, sqsCfg, sqsBreaker, "outbox_dispatcher")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dispatcher = appworker.NewOutboxDispatcher(queueCfg, authRepo, rawProducer, appLogger)
|
||||
}
|
||||
|
||||
emailSender, err := buildEmailSender(ctx, emailCfg, appCfg.SES, resCfg, appLogger)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
handler := service.NewEmailDeliveryHandler(emailSender, appLogger)
|
||||
runner := appworker.NewRunner(queueCfg, consumer, serializer, handler, idempotency, dispatcher, appLogger)
|
||||
|
||||
appLogger.Info("worker started",
|
||||
"backend", "sqs",
|
||||
"email_provider", emailCfg.Provider,
|
||||
"monitor_addr", queueCfg.WorkerMonitorAddr,
|
||||
"outbox_enabled", queueCfg.OutboxEnabled,
|
||||
)
|
||||
if err := runner.Start(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
appLogger.Info("worker stopped")
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildEmailSender(
|
||||
ctx context.Context,
|
||||
emailCfg config.EmailDeliveryConfig,
|
||||
sesCfg config.SESConfig,
|
||||
resCfg config.ResilienceConfig,
|
||||
appLogger *slog.Logger,
|
||||
) (service.EmailSender, error) {
|
||||
switch emailCfg.Provider {
|
||||
case "", "ses":
|
||||
sesBreaker := resilience.NewExecutor("ses", resCfg.SES, appLogger, resilience.ClassifySESError)
|
||||
sender, err := service.NewSESSender(ctx, sesCfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sender = sender.WithExecutor(sesBreaker)
|
||||
sender = sender.WithMetricsComponent("email_worker")
|
||||
if !sender.Configured() {
|
||||
return nil, errors.New("ses is not configured")
|
||||
}
|
||||
return sender, nil
|
||||
case "mock":
|
||||
sender := service.NewMockEmailSender(emailCfg.MockDir)
|
||||
if !sender.Configured() {
|
||||
return nil, errors.New("mock email sender is not configured")
|
||||
}
|
||||
return sender, nil
|
||||
default:
|
||||
return nil, errors.New("unsupported email provider: " + emailCfg.Provider)
|
||||
}
|
||||
}
|
||||
80
cmd/worker/main.go
Normal file
80
cmd/worker/main.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"wucher/internal/config"
|
||||
)
|
||||
|
||||
const (
|
||||
workerJobEnvKey = "WORKER_JOB"
|
||||
workerJobEmail = "email"
|
||||
workerJobFileProcessing = "file_processing"
|
||||
workerJobAliasMail = "mail"
|
||||
workerJobAliasEmailWorker = "email_worker"
|
||||
workerJobAliasFile = "file"
|
||||
workerJobAliasFileWorker = "file_worker"
|
||||
)
|
||||
|
||||
var (
|
||||
workerRunFn = run
|
||||
workerFatalFn = log.Fatal
|
||||
runMailWorkerFn func(config.AppConfig) error = runMailWorker
|
||||
runFileWorkerFn func(config.AppConfig) error = runFileWorker
|
||||
)
|
||||
|
||||
func configFilePath() string {
|
||||
if v := os.Getenv("CONFIG_FILE"); v != "" {
|
||||
return v
|
||||
}
|
||||
basePath := os.Getenv("CONFIG_BASE_PATH")
|
||||
if basePath == "" {
|
||||
basePath = "/tmp"
|
||||
}
|
||||
filename := os.Getenv("CONFIG_FILENAME")
|
||||
if filename == "" {
|
||||
filename = "config.generated.json"
|
||||
}
|
||||
return basePath + "/" + filename
|
||||
}
|
||||
|
||||
func main() {
|
||||
if err := workerRunFn(); err != nil {
|
||||
workerFatalFn(err)
|
||||
}
|
||||
}
|
||||
|
||||
func run() error {
|
||||
appCfg, err := config.LoadFromFile(configFilePath())
|
||||
if err != nil {
|
||||
return fmt.Errorf("load config: %w", err)
|
||||
}
|
||||
|
||||
job := normalizeWorkerJob(os.Getenv(workerJobEnvKey))
|
||||
|
||||
switch job {
|
||||
case workerJobEmail:
|
||||
return runMailWorkerFn(appCfg)
|
||||
case workerJobFileProcessing:
|
||||
return runFileWorkerFn(appCfg)
|
||||
default:
|
||||
return errors.New("unsupported worker job: " + job)
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeWorkerJob(raw string) string {
|
||||
job := strings.ToLower(strings.TrimSpace(raw))
|
||||
|
||||
switch job {
|
||||
case "", workerJobEmail, workerJobAliasMail, workerJobAliasEmailWorker:
|
||||
return workerJobEmail
|
||||
case workerJobFileProcessing, workerJobAliasFile, workerJobAliasFileWorker:
|
||||
return workerJobFileProcessing
|
||||
default:
|
||||
return job
|
||||
}
|
||||
}
|
||||
136
cmd/worker/main_test.go
Normal file
136
cmd/worker/main_test.go
Normal file
@@ -0,0 +1,136 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"wucher/internal/config"
|
||||
"wucher/internal/shared/pkg/logger"
|
||||
)
|
||||
|
||||
// writeTestConfig writes a minimal valid JSON config to a temp file and sets
|
||||
// CONFIG_FILE so that run() can load it.
|
||||
func writeTestConfig(t *testing.T) {
|
||||
t.Helper()
|
||||
f := filepath.Join(t.TempDir(), "config.json")
|
||||
if err := os.WriteFile(f, []byte("{}"), 0o600); err != nil {
|
||||
t.Fatalf("writeTestConfig: %v", err)
|
||||
}
|
||||
t.Setenv("CONFIG_FILE", f)
|
||||
}
|
||||
|
||||
func TestMain_CallsRun(t *testing.T) {
|
||||
origRun := workerRunFn
|
||||
t.Cleanup(func() { workerRunFn = origRun })
|
||||
|
||||
runCalled := false
|
||||
workerRunFn = func() error {
|
||||
runCalled = true
|
||||
return nil
|
||||
}
|
||||
|
||||
main()
|
||||
|
||||
if !runCalled {
|
||||
t.Fatalf("expected worker run function to be called")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRun_DefaultsToMailWorker(t *testing.T) {
|
||||
origMail := runMailWorkerFn
|
||||
origFile := runFileWorkerFn
|
||||
t.Cleanup(func() {
|
||||
runMailWorkerFn = origMail
|
||||
runFileWorkerFn = origFile
|
||||
})
|
||||
|
||||
writeTestConfig(t)
|
||||
t.Setenv(workerJobEnvKey, "")
|
||||
mailCalled := false
|
||||
fileCalled := false
|
||||
runMailWorkerFn = func(config.AppConfig) error {
|
||||
mailCalled = true
|
||||
return nil
|
||||
}
|
||||
runFileWorkerFn = func(config.AppConfig) error {
|
||||
fileCalled = true
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := run(); err != nil {
|
||||
t.Fatalf("run: %v", err)
|
||||
}
|
||||
if !mailCalled {
|
||||
t.Fatalf("expected mail worker to be called")
|
||||
}
|
||||
if fileCalled {
|
||||
t.Fatalf("expected file worker not to be called")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRun_UsesFileWorkerWhenConfigured(t *testing.T) {
|
||||
origMail := runMailWorkerFn
|
||||
origFile := runFileWorkerFn
|
||||
t.Cleanup(func() {
|
||||
runMailWorkerFn = origMail
|
||||
runFileWorkerFn = origFile
|
||||
})
|
||||
|
||||
writeTestConfig(t)
|
||||
t.Setenv(workerJobEnvKey, workerJobFileProcessing)
|
||||
mailCalled := false
|
||||
fileCalled := false
|
||||
runMailWorkerFn = func(config.AppConfig) error {
|
||||
mailCalled = true
|
||||
return nil
|
||||
}
|
||||
runFileWorkerFn = func(config.AppConfig) error {
|
||||
fileCalled = true
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := run(); err != nil {
|
||||
t.Fatalf("run: %v", err)
|
||||
}
|
||||
if !fileCalled {
|
||||
t.Fatalf("expected file worker to be called")
|
||||
}
|
||||
if mailCalled {
|
||||
t.Fatalf("expected mail worker not to be called")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRun_ReturnsErrorOnUnsupportedWorkerJob(t *testing.T) {
|
||||
writeTestConfig(t)
|
||||
t.Setenv(workerJobEnvKey, "unknown_worker")
|
||||
|
||||
err := run()
|
||||
if err == nil || !strings.Contains(err.Error(), "unsupported worker job") {
|
||||
t.Fatalf("expected unsupported worker job error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildEmailSenderMock(t *testing.T) {
|
||||
sender, err := buildEmailSender(context.Background(), config.EmailDeliveryConfig{
|
||||
Provider: "mock",
|
||||
MockDir: t.TempDir(),
|
||||
}, config.SESConfig{}, config.ResilienceConfig{}, logger.NewDiscard())
|
||||
if err != nil {
|
||||
t.Fatalf("buildEmailSender: %v", err)
|
||||
}
|
||||
if sender == nil {
|
||||
t.Fatalf("expected sender")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildEmailSenderUnsupportedProvider(t *testing.T) {
|
||||
_, err := buildEmailSender(context.Background(), config.EmailDeliveryConfig{
|
||||
Provider: "unknown",
|
||||
}, config.SESConfig{}, config.ResilienceConfig{}, logger.NewDiscard())
|
||||
if err == nil || !strings.Contains(err.Error(), "unsupported email provider") {
|
||||
t.Fatalf("expected unsupported provider error, got %v", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user