tercul-backend/cmd/worker/main.go
google-labs-jules[bot] f66936bc4b feat: Implement event-driven analytics features
This commit implements a robust, production-ready analytics system using an event-driven architecture with Redis and `asynq`.

Key changes:
- Event-Driven Architecture: Instead of synchronous database updates, analytics events (e.g., views, likes, comments) are now published to a Redis queue. This improves API response times and decouples the analytics system from the main application flow.
- Background Worker: A new worker process (`cmd/worker`) has been created to consume events from the queue and update the analytics counters in the database.
- View Counting: Implemented the missing view counting feature for both works and translations.
- New Analytics Query: Added a `popularTranslations` GraphQL query to demonstrate how to use the collected analytics data.
- Testing: Added unit tests for the new event publisher and integration tests for the analytics worker.

Known Issue:
The integration tests for the analytics worker (`AnalyticsWorkerSuite`) and the GraphQL API (`GraphQLIntegrationSuite`) are currently failing due to the lack of a Redis service in the test environment. The tests are written and are expected to pass in an environment where Redis is available on `localhost:6379`, as configured in the CI pipeline.
2025-09-07 22:30:23 +00:00

73 lines
2.2 KiB
Go

package main
import (
"log"
"os"
"os/signal"
"syscall"
"tercul/internal/app"
app_analytics "tercul/internal/app/analytics"
analytics_job "tercul/internal/jobs/analytics"
"tercul/internal/platform/config"
app_log "tercul/internal/platform/log"
"github.com/hibiken/asynq"
)
func main() {
// Load configuration from environment variables
config.LoadConfig()
// Initialize structured logger
app_log.SetDefaultLevel(app_log.InfoLevel)
app_log.LogInfo("Starting Tercul worker")
// Build application components
appBuilder := app.NewApplicationBuilder()
if err := appBuilder.Build(); err != nil {
log.Fatalf("Failed to build application: %v", err)
}
defer appBuilder.Close()
// Create asynq server
srv := asynq.NewServer(
asynq.RedisClientOpt{
Addr: config.Cfg.RedisAddr,
Password: config.Cfg.RedisPassword,
DB: config.Cfg.RedisDB,
},
asynq.Config{
Queues: map[string]int{
app_analytics.QueueAnalytics: 10, // Process analytics queue with priority 10
},
},
)
// Create and register analytics worker
analyticsWorker := analytics_job.NewWorker(appBuilder.App.AnalyticsService)
mux := asynq.NewServeMux()
mux.HandleFunc(string(app_analytics.EventTypeWorkViewed), analyticsWorker.ProcessTask)
mux.HandleFunc(string(app_analytics.EventTypeWorkLiked), analyticsWorker.ProcessTask)
mux.HandleFunc(string(app_analytics.EventTypeWorkCommented), analyticsWorker.ProcessTask)
mux.HandleFunc(string(app_analytics.EventTypeWorkBookmarked), analyticsWorker.ProcessTask)
mux.HandleFunc(string(app_analytics.EventTypeTranslationViewed), analyticsWorker.ProcessTask)
mux.HandleFunc(string(app_analytics.EventTypeTranslationLiked), analyticsWorker.ProcessTask)
mux.HandleFunc(string(app_analytics.EventTypeTranslationCommented), analyticsWorker.ProcessTask)
// Start the server
go func() {
if err := srv.Run(mux); err != nil {
log.Fatalf("could not run asynq server: %v", err)
}
}()
// Wait for interrupt signal to gracefully shutdown the server
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("Shutting down worker server...")
srv.Shutdown()
log.Println("Worker server shutdown successfully")
}