Real-World Scenario: Moodle LMS Plugin Integration
The Problem
Moodle (and LMS platforms in general) run on PHP + MySQL/Postgres. When a student submits an assignment, the typical synchronous flow blocks the HTTP response until every downstream operation completes:
- Save submission to database
- Run AI-powered plagiarism analysis
- Generate PDF report
- Send email notifications
- Update grade book
- Trigger SCORM tracking
This makes response times unpredictable. On heavy load (end-of-term submission spikes), database connections pile up, timeouts occur, and students see error pages.
The root issue: PHP's synchronous execution model is doing too much in a single HTTP request.
The TinyMQ Solution
TinyMQ acts as a decoupling layer between Moodle's synchronous web tier and any async processing workers.
The PHP plugin publishes a lightweight event payload to TinyMQ in < 5ms. The HTTP response returns immediately. Background Go (or any language) workers consume the event and perform the heavy lifting asynchronously.
Architecture
The critical observation: steps 1–3 in Moodle's handler now take < 10ms total. The heavy work (AI analysis, PDF generation, email) happens outside the HTTP request lifecycle.
Moodle Plugin: Publishing Events
In your Moodle plugin's PHP event handler:
<?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;
}
}
Call it from your assignment submission observer:
<?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',
],
];
Go Worker: AI Analysis Pipeline
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)
}
Scaling the Worker Pool
Run multiple worker instances to process submissions in parallel. Each instance competes for the same queue (FIFO, no duplicate delivery):
# docker-compose.yml alongside Moodle
services:
tinymq:
image: flez71/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
Three replicas means three concurrent analyses. TinyMQ guarantees each message is delivered to exactly one worker.
Monitoring Failed Submissions (DLQ)
If a submission fails processing 3 times (network error, AI service down, etc.), it lands in lms.submissions.dlq. Monitor it from the dashboard or 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
Key Benefits for Moodle Admins
| Before TinyMQ | After TinyMQ |
|---|---|
| Submission endpoint blocks for 2–8s | Submission endpoint returns in < 100ms |
| AI analysis runs in PHP HTTP request | AI analysis runs in dedicated Go worker |
| Database connection held during analysis | DB connection released immediately |
| End-of-term spikes cause timeouts | Queue absorbs spikes; workers drain at their own pace |
| Failed analyses lost silently | Failed events land in DLQ for inspection |