Caso Real: Integración del Plugin Moodle LMS
El Problema
Moodle (y las plataformas LMS en general) funcionan sobre PHP + MySQL/Postgres. Cuando un estudiante entrega una tarea, el flujo síncrono típico bloquea la respuesta HTTP hasta que cada operación posterior se completa:
- Guardar la entrega en la base de datos
- Ejecutar el análisis de plagio con IA
- Generar el informe PDF
- Enviar notificaciones por email
- Actualizar el libro de calificaciones
- Disparar el tracking SCORM
Esto hace que los tiempos de respuesta sean impredecibles. Con carga alta (picos de entregas a fin de trimestre), las conexiones a la base de datos se acumulan, se producen timeouts y los estudiantes ven páginas de error.
El problema de raíz: el modelo de ejecución síncrona de PHP está haciendo demasiado en una sola petición HTTP.
La Solución TinyMQ
TinyMQ actúa como una capa de desacoplamiento entre el tier web síncrono de Moodle y cualquier worker de procesamiento asíncrono.
El plugin PHP publica un payload de evento ligero a TinyMQ en < 5ms. La respuesta HTTP retorna de inmediato. Workers en segundo plano (Go o cualquier lenguaje) consumen el evento y realizan el trabajo pesado de forma asíncrona.
Arquitectura
La observación clave: los pasos 1–3 en el handler de Moodle ahora toman < 10ms en total. El trabajo pesado (análisis IA, generación de PDF, email) ocurre fuera del ciclo de vida de la petición HTTP.
Plugin Moodle: Publicar Eventos
En el handler de eventos PHP de tu plugin Moodle:
<?php
// File: local/tinymq_integration/classes/event/submission_created.php
namespace local_tinymq_integration\event;
class submission_created extends \core\event\base {
public static function send_to_queue(array $submissionData): bool {
$tinymqUrl = get_config('local_tinymq_integration', 'broker_url')
?? 'http://tinymq:7800';
$payload = json_encode([
'submission_id' => $submissionData['id'],
'user_id' => $submissionData['userid'],
'course_id' => $submissionData['courseid'],
'assignment_id' => $submissionData['assignmentid'],
'file_url' => $submissionData['file_url'],
'timestamp' => time(),
]);
// Non-blocking: this call completes in < 5ms
$ch = curl_init("$tinymqUrl/publish/lms.submissions");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 3, // Hard timeout: never block for more than 3s
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
return $httpCode === 202;
}
}
Llámalo desde el observer de entrega de tareas:
<?php
// File: local/tinymq_integration/db/events.php
$observers = [
[
'eventname' => '\assignsubmission_file\event\assessable_uploaded',
'callback' => '\local_tinymq_integration\event\submission_created::send_to_queue',
],
];
Worker Go: Pipeline de Análisis IA
package main
import (
"encoding/json"
"fmt"
"log"
"github.com/x-name15/tinymq/client"
"github.com/x-name15/tinymq/internal/message"
)
type SubmissionEvent struct {
SubmissionID int `json:"submission_id"`
UserID int `json:"user_id"`
CourseID int `json:"course_id"`
AssignmentID int `json:"assignment_id"`
FileURL string `json:"file_url"`
Timestamp int64 `json:"timestamp"`
}
func analyzeSubmission(msg message.Message) error {
var event SubmissionEvent
if err := json.Unmarshal(msg.Payload, &event); err != nil {
// Malformed event — will go to lms.submissions.dlq after 3 retries
return fmt.Errorf("invalid event payload: %w", err)
}
log.Printf("Analyzing submission %d for user %d...\n", event.SubmissionID, event.UserID)
// Run AI plagiarism check (hypothetical)
score, err := runPlagiarismCheck(event.FileURL)
if err != nil {
return fmt.Errorf("analysis failed: %w", err)
}
// Write result back to Moodle's DB
if err := savePlagiarismScore(event.SubmissionID, score); err != nil {
return fmt.Errorf("DB write failed: %w", err)
}
log.Printf("Submission %d: plagiarism score = %.2f%%\n", event.SubmissionID, score)
return nil
}
func main() {
mq := client.NewClient("http://tinymq:7800")
log.Println("LMS Analysis Worker started. Waiting for submissions...")
// This blocks forever, processing one submission at a time
// Exponential backoff + DLQ on 3 failures — no extra config needed
mq.Subscribe("lms.submissions", client.SubscriptionOptions{
Timeout: "15s",
}, analyzeSubmission)
}
Escalar el Pool de Workers
Ejecuta múltiples instancias de worker para procesar entregas en paralelo. Cada instancia compite por la misma queue (FIFO, sin entrega duplicada):
# docker-compose.yml alongside Moodle
services:
tinymq:
image: ghcr.io/x-name15/tinymq:latest
ports:
- "7800:7800"
volumes:
- ./data:/root/data
restart: unless-stopped
lms-worker:
build: ./workers/lms-analysis
environment:
- TINYMQ_URL=http://tinymq:7800
- DB_HOST=moodle-db
deploy:
replicas: 3 # 3 parallel workers
restart: unless-stopped
depends_on:
- tinymq
Tres réplicas significan tres análisis concurrentes. TinyMQ garantiza que cada mensaje se entrega exactamente a un worker.
Monitorizar Entregas Fallidas (DLQ)
Si una entrega falla el procesamiento 3 veces (error de red, servicio de IA caído, etc.), aterriza en lms.submissions.dlq. Monitorízalo desde el dashboard o CLI:
# Check DLQ size
tmq status | grep dlq
# Inspect failed submissions without removing them
tmq peek lms.submissions.dlq --limit=5
# Re-process after fixing the root cause
tmq sub lms.submissions.dlq --limit=10 | xargs re-publish
Beneficios Clave para Administradores de Moodle
| Antes de TinyMQ | Después de TinyMQ |
|---|---|
| El endpoint de entrega bloquea 2–8s | El endpoint de entrega responde en < 100ms |
| El análisis IA se ejecuta en la petición HTTP de PHP | El análisis IA se ejecuta en un worker Go dedicado |
| La conexión a la base de datos se retiene durante el análisis | La conexión a la BD se libera de inmediato |
| Los picos de fin de trimestre causan timeouts | La queue absorbe los picos; los workers drenan a su propio ritmo |
| Los análisis fallidos se pierden silenciosamente | Los eventos fallidos aterrizan en el DLQ para su inspección |