# Job Worker System - Documentation Complète **Date** : 2025-12-05 **Version** : 1.0 **Statut** : ✅ **IMPLÉMENTÉ** ## Table des Matières 1. [Vue d'ensemble](#vue-densemble) 2. [Architecture](#architecture) 3. [Types de Jobs](#types-de-jobs) 4. [API et Utilisation](#api-et-utilisation) 5. [Configuration](#configuration) 6. [Tests](#tests) 7. [Monitoring et Observabilité](#monitoring-et-observabilité) 8. [Guide d'Intégration](#guide-dintégration) 9. [Troubleshooting](#troubleshooting) --- ## Vue d'ensemble Le système de Job Worker de Veza permet d'exécuter des tâches asynchrones en arrière-plan, garantissant que les opérations longues ou non critiques n'impactent pas la performance de l'API. ### Fonctionnalités - ✅ **Queue in-memory** avec workers pool - ✅ **Retry automatique** avec exponential backoff - ✅ **Support de plusieurs types de jobs** (Email, Thumbnail, Analytics) - ✅ **Logging structuré** avec zap - ✅ **Gestion d'erreurs robuste** - ✅ **Priorités de jobs** (1 = haut, 2 = moyen, 3 = bas) ### Types de Jobs Implémentés 1. **EmailJob** : Envoi d'emails transactionnels via SMTP 2. **ThumbnailJob** : Génération de thumbnails d'images 3. **AnalyticsEventJob** : Enregistrement d'événements analytics génériques --- ## Architecture ### Schéma Global ``` ┌─────────────────┐ │ API Handler │ │ (Gin Handler) │ └────────┬────────┘ │ │ EnqueueJob() ▼ ┌─────────────────┐ │ JobWorker │ │ │ │ - Queue (chan) │ │ - Workers (N) │ │ - Retry Logic │ └────────┬────────┘ │ │ Dispatch by Type ▼ ┌─────────────────┬─────────────────┬──────────────────────┐ │ EmailJob │ ThumbnailJob │ AnalyticsEventJob │ │ │ │ │ │ - Render │ - Resize │ - Store │ │ Template │ Image │ Event │ │ - Send SMTP │ - Save File │ - JSON Payload │ └─────────────────┴─────────────────┴─────────────────┘ ``` ### Composants Principaux #### 1. JobWorker (`internal/workers/job_worker.go`) Structure centrale qui gère : - La queue de jobs (`chan Job`) - Le pool de workers - Le dispatch par type - La logique de retry ```go type JobWorker struct { db *gorm.DB jobService *services.JobService logger *zap.Logger queue chan Job maxRetries int processingWorkers int emailSender email.EmailSender } ``` #### 2. Job Interface Tous les jobs implémentent l'interface `Job` : ```go type Job struct { ID uuid.UUID Type string // "email", "thumbnail", "analytics" Payload map[string]interface{} Retries int CreatedAt time.Time Priority int // 1 = haut, 2 = moyen, 3 = bas } ``` #### 3. Workers Pool Par défaut, le nombre de workers est configuré à `3` (modifiable dans `config.go`). Chaque worker : - Lit depuis la queue - Exécute le job via `executeJob()` - Gère les retries en cas d'échec - Log les résultats --- ## Types de Jobs ### 1. EmailJob **Fichier** : `internal/workers/email_job.go` **Description** : Envoie des emails transactionnels via SMTP avec support de templates HTML. **Utilisation** : ```go // Email simple jobWorker.EnqueueEmailJob( "user@example.com", "Welcome to Veza", "
Thanks for joining.
", ) // Email avec template jobWorker.EnqueueEmailJobWithTemplate( "user@example.com", "Reset your password", "password_reset", map[string]interface{}{ "Username": "john_doe", "ResetURL": "https://veza.app/reset?token=...", }, ) ``` **Templates disponibles** : - `templates/email/password_reset.html` - `templates/email/welcome.html` **Configuration SMTP** : Variables d'environnement (voir [Configuration](#configuration)) --- ### 2. ThumbnailJob **Fichier** : `internal/workers/thumbnail_job.go` **Description** : Génère des thumbnails d'images avec redimensionnement et compression. **Utilisation** : ```go jobWorker.EnqueueThumbnailJob( "/uploads/images/original.jpg", // Input path "/uploads/thumbnails/thumb.jpg", // Output path 300, // Width (px) 300, // Height (px) ) ``` **Caractéristiques** : - Support formats : JPEG, PNG, GIF, BMP - Algorithme : Lanczos (haute qualité) - Dimensions par défaut : 300x300px si non spécifiées - Création automatique du répertoire de sortie **Exemple d'intégration** : ```go // Dans un handler d'upload d'image func (h *ImageHandler) UploadImage(c *gin.Context) { // ... upload du fichier original ... // Enqueue thumbnail generation if h.jobWorker != nil { thumbnailPath := filepath.Join("thumbnails", filepath.Base(originalPath)) h.jobWorker.EnqueueThumbnailJob(originalPath, thumbnailPath, 300, 300) } } ``` --- ### 3. AnalyticsEventJob **Fichier** : `internal/workers/analytics_job.go` **Description** : Enregistre des événements analytics génériques dans la table `analytics_events`. **Note** : Ne pas confondre avec `AnalyticsJob` dans `playback_analytics_worker.go` qui est spécifique aux analytics de lecture. **Utilisation** : ```go // Événement avec userID userID := uuid.New() jobWorker.EnqueueAnalyticsJob( "track_play", &userID, map[string]interface{}{ "track_id": trackID.String(), "duration": 120, "device": "web", }, ) // Événement anonyme jobWorker.EnqueueAnalyticsJob( "page_view", nil, // Pas de userID map[string]interface{}{ "path": "/tracks", "referrer": "https://google.com", }, ) ``` **Table de base de données** : ```sql CREATE TABLE analytics_events ( id UUID PRIMARY KEY, event_name VARCHAR(100) NOT NULL, user_id UUID REFERENCES users(id), payload JSONB NOT NULL, created_at TIMESTAMPTZ NOT NULL ); ``` **Indexes** : - `idx_analytics_events_name` : Sur `event_name` - `idx_analytics_events_user_id` : Sur `user_id` (partiel, WHERE user_id IS NOT NULL) - `idx_analytics_events_created_at` : Sur `created_at DESC` - `idx_analytics_events_payload_gin` : GIN index sur `payload` (JSONB) **Exemple d'intégration** : ```go // Dans un handler de lecture de track func (h *TrackHandler) PlayTrack(c *gin.Context) { trackID := c.Param("id") userID := c.MustGet("user_id").(uuid.UUID) // ... logique de lecture ... // Enqueue analytics event if h.jobWorker != nil { h.jobWorker.EnqueueAnalyticsJob( "track_play", &userID, map[string]interface{}{ "track_id": trackID, "timestamp": time.Now().Unix(), }, ) } } ``` --- ## API et Utilisation ### Initialisation Le JobWorker est initialisé automatiquement dans `config.go` : ```go config.JobWorker = workers.NewJobWorker( config.Database.GormDB, jobService, logger, 100, // queueSize 3, // workers 3, // maxRetries config.EmailSender, ) ``` Et démarré dans `cmd/api/main.go` : ```go if cfg.JobWorker != nil { workerCtx, workerCancel := context.WithCancel(context.Background()) defer workerCancel() cfg.JobWorker.Start(workerCtx) logger.Info("✅ Job Worker démarré") } ``` ### Méthodes Publiques #### Enqueue ```go // Ajouter un job à la queue jobWorker.Enqueue(job Job) ``` #### Helpers par Type ```go // Email jobWorker.EnqueueEmailJob(to, subject, body string) jobWorker.EnqueueEmailJobWithTemplate(to, subject, templateName string, templateData map[string]interface{}) // Thumbnail jobWorker.EnqueueThumbnailJob(inputPath, outputPath string, width, height int) // Analytics jobWorker.EnqueueAnalyticsJob(eventName string, userID *uuid.UUID, payload map[string]interface{}) ``` #### Statistiques ```go stats := jobWorker.GetStats() // Retourne : queue_size, workers, max_retries ``` --- ## Configuration ### Variables d'Environnement #### SMTP (pour EmailJob) ```bash # Production SMTP_HOST=smtp.gmail.com SMTP_PORT=587 SMTP_USERNAME=your-email@gmail.com SMTP_PASSWORD=your-app-password SMTP_FROM=noreply@veza.app SMTP_FROM_NAME=Veza # Développement (MailHog) MAILHOG_HOST=localhost MAILHOG_PORT=1025 ``` #### Job Worker ```bash # Optionnel - valeurs par défaut utilisées si non définies JOB_WORKER_QUEUE_SIZE=100 JOB_WORKER_WORKERS=3 JOB_WORKER_MAX_RETRIES=3 ``` ### Configuration dans Code Modifier `internal/config/config.go` pour ajuster les paramètres : ```go config.JobWorker = workers.NewJobWorker( config.Database.GormDB, jobService, logger, queueSize, // Taille de la queue workers, // Nombre de workers maxRetries, // Nombre max de retries config.EmailSender, ) ``` --- ## Tests ### Tests Unitaires ```bash # Tous les tests workers go test ./internal/workers/... -v # Tests spécifiques go test ./internal/workers/thumbnail_job_test.go -v go test ./internal/workers/analytics_job_test.go -v go test ./internal/workers/email_job_test.go -v ``` ### Tests d'Intégration Pour tester le système complet : 1. **Email** : Démarrer MailHog et vérifier la réception 2. **Thumbnail** : Uploader une image et vérifier la génération 3. **Analytics** : Déclencher un événement et vérifier la table DB --- ## Monitoring et Observabilité ### Logs Le JobWorker log tous les événements importants : ``` INFO Job worker started workers=3 INFO Processing job job_id=... job_type=email worker_id=0 INFO Email job executed successfully to=user@example.com ERROR Job execution failed job_id=... error=... INFO Retrying job new_retries=1 ``` ### Métriques (Futur) Les métriques Prometheus peuvent être ajoutées pour : - Nombre de jobs enqueue - Taux de succès/échec par type - Temps d'exécution moyen - Taille de la queue --- ## Guide d'Intégration ### Ajouter un Nouveau Type de Job 1. **Créer le fichier job** : `internal/workers/my_job.go` ```go package workers type MyJob struct { Field1 string Field2 int } func (j *MyJob) Execute(ctx context.Context, logger *zap.Logger) error { // Implémentation return nil } ``` 2. **Ajouter le handler dans `job_worker.go`** : ```go func (w *JobWorker) executeJob(ctx context.Context, job Job) error { switch job.Type { case "email": return w.processEmailJob(ctx, job) case "thumbnail": return w.processThumbnailJob(ctx, job) case "analytics": return w.processAnalyticsJob(ctx, job) case "my_job": // Nouveau return w.processMyJob(ctx, job) default: return fmt.Errorf("unknown job type: %s", job.Type) } } func (w *JobWorker) processMyJob(ctx context.Context, job Job) error { // Extraire payload field1, _ := job.Payload["field1"].(string) // Créer et exécuter myJob := NewMyJob(field1, ...) return myJob.Execute(ctx, w.logger) } ``` 3. **Ajouter un helper d'enqueue** : ```go func (w *JobWorker) EnqueueMyJob(field1 string, field2 int) { job := Job{ Type: "my_job", Priority: 2, Payload: map[string]interface{}{ "field1": field1, "field2": field2, }, } w.Enqueue(job) } ``` ### Intégrer dans un Handler ```go type MyHandler struct { jobWorker *workers.JobWorker // ... autres champs } func (h *MyHandler) MyAction(c *gin.Context) { // ... logique métier ... // Enqueue job asynchrone if h.jobWorker != nil { h.jobWorker.EnqueueMyJob("value1", 42) } } ``` --- ## Troubleshooting ### Problèmes Courants #### 1. Jobs non exécutés **Symptôme** : Les jobs restent dans la queue sans être traités. **Solutions** : - Vérifier que `JobWorker.Start()` est appelé dans `main.go` - Vérifier les logs pour erreurs de workers - Vérifier que la queue n'est pas pleine (`GetStats()`) #### 2. Emails non envoyés **Symptôme** : Les jobs email sont enqueue mais aucun email n'est reçu. **Solutions** : - Vérifier la configuration SMTP - Vérifier les logs pour erreurs SMTP - En dev, vérifier que MailHog est démarré #### 3. Thumbnails non générés **Symptôme** : Les jobs thumbnail échouent. **Solutions** : - Vérifier que le fichier source existe - Vérifier les permissions d'écriture sur le répertoire de sortie - Vérifier que le format d'image est supporté #### 4. Analytics non enregistrés **Symptôme** : Les événements analytics ne sont pas dans la DB. **Solutions** : - Vérifier que la migration `043_analytics_events.sql` est appliquée - Vérifier les logs pour erreurs DB - Vérifier la connexion DB ### Logs de Debug Activer les logs de debug : ```go logger, _ := zap.NewDevelopment() ``` --- ## Limitations Actuelles 1. **Queue in-memory** : Jobs perdus au redémarrage 2. **Pas de dead-letter queue** : Échecs définitifs juste loggés 3. **Pas de priorités dynamiques** : Priorité fixée à l'enqueue 4. **Pas de métriques Prometheus** : À implémenter Ces limitations sont acceptables pour P1 et peuvent être adressées en P2. --- ## Roadmap Future (P2) - [ ] Queue persistante (Redis, RabbitMQ) - [ ] Dead letter queue - [ ] Métriques Prometheus - [ ] Dashboard de monitoring - [ ] Rate limiting par type de job - [ ] Support de jobs récurrents (cron) --- **Documentation mise à jour le** : 2025-12-05 **Auteur** : Veza Backend Team