PHP
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Functions
-
Section 4: Object-Oriented PHP
-
Section 5: Working with Data
-
Section 6: Modern PHP (PHP 8)
-
Section 7: Working with Files and Networking
-
Section 8: Common PHP Frameworks Overview
-
Section 9: Tooling and Ecosystem
-
Section 10: Practical Projects
-
Section 11: Interview Practice
-
Section 12: More Standard Library
-
Section 13: Data Structures and Algorithms in PHP
-
Section 14: More Practice Exercises
-
Section 15: More Security and Best Practices
-
Section 16: More Testing and Tooling
-
Section 17: WordPress-Style CMS Concepts
-
Section 18: Advanced OOP Practice
-
Section 19: More Web Fundamentals
-
Section 20: Database Practice
-
Section 21: PHP Manual: Array Functions
-
Section 22: PHP Manual: Date and Calendar Functions
-
Section 23: PHP Manual: Filesystem and Directory Functions
-
Section 24: PHP Manual: Filter and Var Handling
-
Section 25: PHP Manual: Math Functions
-
Section 26: PHP Manual: JSON and XML
-
Section 27: PHP Manual: Network and Stream Functions
-
Section 28: PHP Manual: Error and Exception Handling
-
Section 29: PHP Manual: Output Control and Misc
-
Section 30: PHP Manual: FTP, Zip, and Mail
-
Section 31: Modern PHP Frameworks Deep Dive
-
Section 32: PHP Design Patterns
-
Section 33: More Practice Exercises
-
Section 34: PHP Performance and Deployment
-
Section 35: More Interview Practice
-
Section 36: More PHP Standard Library
-
Section 37: PHP Concurrency and Async
-
Section 38: More Web Development Practice
-
Section 39: PHP Testing Deep Dive
-
Section 40: Composer and Package Development
-
Section 41: PHP Security Deep Dive
-
Section 42: More Practical Projects
-
Section 43: Legacy PHP Maintenance
-
Section 44: More Algorithm Practice
-
Section 45: Final Practice and Review
-
Section 46: PHP for E-Commerce Patterns
-
Section 47: PHP API Design Deep Dive
-
Section 48: PHP Caching Strategies
-
Section 49: PHP Queue and Background Jobs
-
Section 50: PHP Multi-Tenancy Patterns
-
Section 51: PHP Real-Time Features
-
Section 52: PHP CMS and Content Modeling
-
Section 53: PHP Internationalization
-
Section 54: More Framework-Specific Practice
-
Section 55: PHP Legacy Code Refactoring
-
Section 56: More Practice Projects Round 2
-
Section 57: PHP Command-Line Applications
-
Section 58: PHP and Microservices
-
Section 59: More Interview and Review Round 2
207: Building a Simple Job Queue with a Database Table
We've all been there: your user clicks "Sign Up," and the browser just hangs. Why? Because your code is trying to connect to an external SMTP server to send a welcome email, and that server is taking three seconds to respond. The user thinks your site is broken, but in reality, you're just making them wait for a task that doesn't actually need to happen in real-time.
The solution is a job queue. While tools like RabbitMQ or Redis are the industry standard for high-scale apps, you don't always need that complexity. For a lot of projects, a simple database table is more than enough to get the job done.
Designing the holding pen
First, we need a place to store these pending tasks. I like to keep it lean. We need to know what the job is, when it was created, and whether it's already being handled. I'm going to use a payload column to store a JSON string; this gives us the flexibility to pass different types of data (like a user ID and an email template name) without changing the schema every time we add a new job type.
CREATE TABLE job_queue (
id INT AUTO_INCREMENT PRIMARY KEY,
payload TEXT NOT NULL,
status ENUM('pending', 'processing', 'completed', 'failed') DEFAULT 'pending',
attempts INT DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
Offloading the work
Now, instead of calling mail() or a library like PHPMailer directly in our registration logic, we just "dispatch" the job. I've written this as a simple helper function. It just dumps the data into the table and lets the user move on with their life immediately.
function queueJob($jobType, $data) {
$db = getDatabaseConnection();
$payload = json_encode(['type' => $jobType, 'data' => $data]);
$stmt = $db->prepare("INSERT INTO job_queue (payload) VALUES (?)");
$stmt->execute([$payload]);
}
// Usage during registration:
queueJob('send_welcome_email', ['user_id' => 42, 'email' => 'user@example.com']);
The worker script and a critical race condition
Now for the worker. This is a PHP script that runs in the background (usually via a cron job or a supervisor process). It looks for 'pending' jobs and executes them. Initially, I wrote it like this:
// DON'T DO THIS - My first mistake
$jobs = $db->query("SELECT * FROM job_queue WHERE status = 'pending' LIMIT 5");
foreach ($jobs as $job) {
processJob($job);
$db->prepare("UPDATE job_queue SET status = 'completed' WHERE id = ?")->execute([$job['id']]);
}
I realized quickly that this is a disaster waiting to happen. If this script takes 30 seconds to run and your cron job triggers every 10 seconds, you'll have multiple workers picking up the same 'pending' jobs. Your users will get five welcome emails, and you'll look like an amateur.
To fix this, we need to "claim" the job atomically. I'll update the status to 'processing' before I even touch the payload. By using a WHERE status = 'pending' in the update statement, the database ensures only one worker wins the race for that specific ID.
while (true) {
$db = getDatabaseConnection();
// Find one pending job
$stmt = $db->query("SELECT id FROM job_queue WHERE status = 'pending' LIMIT 1");
$job = $stmt->fetch();
if (!$job) {
sleep(5); // Nothing to do, take a nap
continue;
}
// Atomically claim the job
$update = $db->prepare("UPDATE job_queue SET status = 'processing' WHERE id = ? AND status = 'pending'");
$update->execute([$job['id']]);
if ($update->rowCount() === 0) {
continue; // Someone else grabbed it first!
}
// Now we actually do the work
$jobDetails = $db->prepare("SELECT payload FROM job_queue WHERE id = ?");
$jobDetails->execute([$job['id']]);
$data = json_decode($jobDetails->fetchColumn(), true);
try {
if ($data['type'] === 'send_welcome_email') {
// Simulate sending email
echo "Sending email to " . $data['data']['email'] . "\n";
}
$db->prepare("UPDATE job_queue SET status = 'completed' WHERE id = ?")->execute([$job['id']]);
} catch (Exception $e) {
$db->prepare("UPDATE job_queue SET status = 'failed', attempts = attempts + 1 WHERE id = ?")->execute([$job['id']]);
}
}
Keeping the table clean
One last thing: if you leave this running for a year, your job_queue table will grow to millions of rows of 'completed' jobs. It'll eventually slow down your queries. In a real production environment, I'd set up a separate cleanup task to delete completed jobs older than 7 days. A simple DELETE FROM job_queue WHERE status = 'completed' AND updated_at < NOW() - INTERVAL 7 DAY does the trick.
📋 Practical Task
Build a Password Reset Token Queue
Using the logic from this lesson, implement a job queue system for password resets. Instead of sending the email immediately when a user requests a password reset, you must:
- Create a new entry in the
job_queuetable with a type of'send_password_reset'and a payload containing the user's email and a unique reset token. - Modify the worker script to handle the
'send_password_reset'job type. - Ensure the worker correctly handles the "claiming" process to prevent the same reset email from being sent multiple times if multiple worker processes are running.
- Add a check in the worker: if a job has reached 3
attemptswithout succeeding, mark its status as'failed'permanently rather than trying again.
There are no comments for now.