Files
michaelschiemer/src/Framework/Queue/Services/DatabaseDeadLetterQueue.php
Michael Schiemer fc3d7e6357 feat(Production): Complete production deployment infrastructure
- Add comprehensive health check system with multiple endpoints
- Add Prometheus metrics endpoint
- Add production logging configurations (5 strategies)
- Add complete deployment documentation suite:
  * QUICKSTART.md - 30-minute deployment guide
  * DEPLOYMENT_CHECKLIST.md - Printable verification checklist
  * DEPLOYMENT_WORKFLOW.md - Complete deployment lifecycle
  * PRODUCTION_DEPLOYMENT.md - Comprehensive technical reference
  * production-logging.md - Logging configuration guide
  * ANSIBLE_DEPLOYMENT.md - Infrastructure as Code automation
  * README.md - Navigation hub
  * DEPLOYMENT_SUMMARY.md - Executive summary
- Add deployment scripts and automation
- Add DEPLOYMENT_PLAN.md - Concrete plan for immediate deployment
- Update README with production-ready features

All production infrastructure is now complete and ready for deployment.
2025-10-25 19:18:37 +02:00

205 lines
6.7 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Framework\Queue\Services;
use App\Framework\Database\ConnectionInterface;
use App\Framework\Database\EntityManager;
use App\Framework\Database\ValueObjects\SqlQuery;
use App\Framework\Queue\Contracts\DeadLetterQueueInterface;
use App\Framework\Queue\Queue;
use App\Framework\Queue\Entities\DeadLetterJob;
use App\Framework\Queue\ValueObjects\DeadLetterQueueName;
use App\Framework\Queue\ValueObjects\QueueName;
use App\Framework\Queue\ValueObjects\QueuePriority;
/**
* Database-backed Dead Letter Queue implementation
*/
final readonly class DatabaseDeadLetterQueue implements DeadLetterQueueInterface
{
public function __construct(
private ConnectionInterface $connection,
private EntityManager $entityManager,
private Queue $originalQueue
) {
}
public function addFailedJob(DeadLetterJob $deadLetterJob): void
{
$this->entityManager->persist($deadLetterJob);
$this->entityManager->flush();
}
public function getJobs(DeadLetterQueueName $deadLetterQueueName, int $limit = 100): array
{
$sql = "
SELECT * FROM dead_letter_jobs
WHERE dead_letter_queue = ?
ORDER BY moved_to_dlq_at DESC
LIMIT ?
";
$result = $this->connection->query(SqlQuery::create($sql, [$deadLetterQueueName->toString(), $limit]));
$rows = $result->fetchAll();
return array_map([$this, 'mapRowToDeadLetterJob'], $rows);
}
public function getJobsByOriginalQueue(QueueName $originalQueue, int $limit = 100): array
{
$sql = "
SELECT * FROM dead_letter_jobs
WHERE original_queue = ?
ORDER BY moved_to_dlq_at DESC
LIMIT ?
";
$result = $this->connection->query(SqlQuery::create($sql, [$originalQueue->toString(), $limit]));
$rows = $result->fetchAll();
return array_map([$this, 'mapRowToDeadLetterJob'], $rows);
}
public function retryJob(string $deadLetterJobId): bool
{
$this->connection->beginTransaction();
try {
// Get the dead letter job
$deadLetterJob = $this->findDeadLetterJob($deadLetterJobId);
if (! $deadLetterJob) {
return false;
}
// Add job back to original queue
$jobPayload = $deadLetterJob->getJobPayload();
$originalQueue = $deadLetterJob->getOriginalQueueName();
$this->originalQueue->push(
payload: $jobPayload,
queueName: $originalQueue,
priority: QueuePriority::NORMAL
);
// Update retry count and timestamp
$updatedJob = $deadLetterJob->withRetryAttempt();
$this->entityManager->persist($updatedJob);
// Delete from dead letter queue
$this->deleteJobById($deadLetterJobId);
$this->entityManager->flush();
$this->connection->commit();
return true;
} catch (\Throwable $e) {
$this->connection->rollback();
throw $e;
}
}
public function deleteJob(string $deadLetterJobId): bool
{
return $this->deleteJobById($deadLetterJobId);
}
public function retryAllJobs(DeadLetterQueueName $deadLetterQueueName): int
{
$jobs = $this->getJobs($deadLetterQueueName, 1000); // Get all jobs
$retriedCount = 0;
foreach ($jobs as $job) {
if ($this->retryJob($job->id)) {
$retriedCount++;
}
}
return $retriedCount;
}
public function clearQueue(DeadLetterQueueName $deadLetterQueueName): int
{
$sql = "DELETE FROM dead_letter_jobs WHERE dead_letter_queue = ?";
return $this->connection->execute(SqlQuery::create($sql, [$deadLetterQueueName->toString()]));
}
public function getQueueStats(DeadLetterQueueName $deadLetterQueueName): array
{
$sql = "
SELECT
COUNT(*) as total_jobs,
AVG(failed_attempts) as avg_failed_attempts,
MAX(failed_attempts) as max_failed_attempts,
AVG(retry_count) as avg_retry_count,
MAX(retry_count) as max_retry_count,
MIN(moved_to_dlq_at) as oldest_job,
MAX(moved_to_dlq_at) as newest_job
FROM dead_letter_jobs
WHERE dead_letter_queue = ?
";
$stats = $this->connection->queryOne(SqlQuery::create($sql, [$deadLetterQueueName->toString()]));
return [
'queue_name' => $deadLetterQueueName->toString(),
'total_jobs' => (int) $stats['total_jobs'],
'avg_failed_attempts' => round((float) $stats['avg_failed_attempts'], 2),
'max_failed_attempts' => (int) $stats['max_failed_attempts'],
'avg_retry_count' => round((float) $stats['avg_retry_count'], 2),
'max_retry_count' => (int) $stats['max_retry_count'],
'oldest_job' => $stats['oldest_job'],
'newest_job' => $stats['newest_job'],
];
}
public function getAvailableQueues(): array
{
$sql = "SELECT DISTINCT dead_letter_queue FROM dead_letter_jobs ORDER BY dead_letter_queue";
$result = $this->connection->query(SqlQuery::create($sql));
$rows = $result->fetchAll();
return array_map(
fn (array $row) => DeadLetterQueueName::fromString($row['dead_letter_queue']),
$rows
);
}
private function findDeadLetterJob(string $deadLetterJobId): ?DeadLetterJob
{
$sql = "SELECT * FROM dead_letter_jobs WHERE id = ?";
$row = $this->connection->queryOne(SqlQuery::create($sql, [$deadLetterJobId]));
return $row ? $this->mapRowToDeadLetterJob($row) : null;
}
private function deleteJobById(string $deadLetterJobId): bool
{
$sql = "DELETE FROM dead_letter_jobs WHERE id = ?";
return $this->connection->execute(SqlQuery::create($sql, [$deadLetterJobId])) > 0;
}
private function mapRowToDeadLetterJob(array $row): DeadLetterJob
{
return new DeadLetterJob(
id: $row['id'],
originalJobId: $row['original_job_id'],
deadLetterQueue: $row['dead_letter_queue'],
originalQueue: $row['original_queue'],
jobPayload: $row['job_payload'],
failureReason: $row['failure_reason'],
exceptionType: $row['exception_type'],
stackTrace: $row['stack_trace'],
failedAttempts: (int) $row['failed_attempts'],
failedAt: $row['failed_at'],
movedToDlqAt: $row['moved_to_dlq_at'],
retryCount: (int) $row['retry_count'],
lastRetryAt: $row['last_retry_at']
);
}
}