- Move 12 markdown files from root to docs/ subdirectories - Organize documentation by category: • docs/troubleshooting/ (1 file) - Technical troubleshooting guides • docs/deployment/ (4 files) - Deployment and security documentation • docs/guides/ (3 files) - Feature-specific guides • docs/planning/ (4 files) - Planning and improvement proposals Root directory cleanup: - Reduced from 16 to 4 markdown files in root - Only essential project files remain: • CLAUDE.md (AI instructions) • README.md (Main project readme) • CLEANUP_PLAN.md (Current cleanup plan) • SRC_STRUCTURE_IMPROVEMENTS.md (Structure improvements) This improves: ✅ Documentation discoverability ✅ Logical organization by purpose ✅ Clean root directory ✅ Better maintainability
617 lines
24 KiB
PHP
617 lines
24 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Framework\Queue\ValueObjects\DeadLetterQueueName;
|
|
use App\Framework\Queue\ValueObjects\QueueName;
|
|
use App\Framework\Queue\ValueObjects\JobId;
|
|
use App\Framework\Queue\ValueObjects\JobPayload;
|
|
use App\Framework\Queue\ValueObjects\QueuePriority;
|
|
use App\Framework\Queue\ValueObjects\FailureReason;
|
|
use App\Framework\Queue\Exceptions\InvalidDeadLetterQueueNameException;
|
|
|
|
describe('DeadLetterQueueName Value Object', function () {
|
|
|
|
describe('Construction and Validation', function () {
|
|
it('can create valid dead letter queue names', function () {
|
|
$validNames = [
|
|
'failed',
|
|
'email_failed',
|
|
'background-jobs-failed',
|
|
'user.registration.failed',
|
|
'queue_123_failed',
|
|
'a', // minimum length
|
|
str_repeat('a', 100) // maximum length
|
|
];
|
|
|
|
foreach ($validNames as $name) {
|
|
$dlqName = DeadLetterQueueName::fromString($name);
|
|
expect($dlqName->toString())->toBe($name);
|
|
}
|
|
});
|
|
|
|
it('rejects invalid dead letter queue names', function () {
|
|
$invalidNames = [
|
|
'', // too short
|
|
str_repeat('a', 101), // too long
|
|
'queue with spaces',
|
|
'queue@invalid',
|
|
'queue#invalid',
|
|
'queue$invalid',
|
|
'queue%invalid',
|
|
'queue&invalid',
|
|
'queue*invalid',
|
|
'queue(invalid)',
|
|
'queue+invalid',
|
|
'queue=invalid',
|
|
'queue[invalid]',
|
|
'queue{invalid}',
|
|
'queue|invalid',
|
|
'queue\\invalid',
|
|
'queue:invalid',
|
|
'queue;invalid',
|
|
'queue"invalid',
|
|
'queue\'invalid',
|
|
'queue<invalid>',
|
|
'queue,invalid',
|
|
'queue?invalid',
|
|
'queue/invalid',
|
|
'queue~invalid',
|
|
'queue`invalid'
|
|
];
|
|
|
|
foreach ($invalidNames as $name) {
|
|
expect(fn() => DeadLetterQueueName::fromString($name))
|
|
->toThrow(InvalidDeadLetterQueueNameException::class);
|
|
}
|
|
});
|
|
|
|
it('is readonly and immutable', function () {
|
|
$dlqName = DeadLetterQueueName::fromString('test-failed');
|
|
|
|
$reflection = new ReflectionClass($dlqName);
|
|
expect($reflection->isReadOnly())->toBeTrue();
|
|
|
|
$nameProperty = $reflection->getProperty('name');
|
|
expect($nameProperty->isReadOnly())->toBeTrue();
|
|
});
|
|
});
|
|
|
|
describe('Factory Methods', function () {
|
|
it('creates default dead letter queue name', function () {
|
|
$defaultDlq = DeadLetterQueueName::default();
|
|
expect($defaultDlq->toString())->toBe('failed');
|
|
});
|
|
|
|
it('creates dead letter queue for specific queue', function () {
|
|
$queueName = QueueName::fromString('email');
|
|
$dlqName = DeadLetterQueueName::forQueue($queueName);
|
|
|
|
expect($dlqName->toString())->toBe('email_failed');
|
|
});
|
|
|
|
it('creates dead letter queue for complex queue names', function () {
|
|
$queueName = QueueName::fromString('user.registration');
|
|
$dlqName = DeadLetterQueueName::forQueue($queueName);
|
|
|
|
expect($dlqName->toString())->toBe('user.registration_failed');
|
|
});
|
|
});
|
|
|
|
describe('Equality and Comparison', function () {
|
|
it('equals() compares names correctly', function () {
|
|
$dlq1 = DeadLetterQueueName::fromString('failed');
|
|
$dlq2 = DeadLetterQueueName::fromString('failed');
|
|
$dlq3 = DeadLetterQueueName::fromString('other_failed');
|
|
|
|
expect($dlq1->equals($dlq2))->toBeTrue();
|
|
expect($dlq1->equals($dlq3))->toBeFalse();
|
|
expect($dlq2->equals($dlq3))->toBeFalse();
|
|
});
|
|
|
|
it('string representation works correctly', function () {
|
|
$name = 'test-failed-queue';
|
|
$dlqName = DeadLetterQueueName::fromString($name);
|
|
|
|
expect($dlqName->toString())->toBe($name);
|
|
expect((string) $dlqName)->toBe($name);
|
|
});
|
|
});
|
|
|
|
describe('Edge Cases', function () {
|
|
it('handles minimum and maximum length names', function () {
|
|
$minName = DeadLetterQueueName::fromString('a');
|
|
expect($minName->toString())->toBe('a');
|
|
|
|
$maxName = DeadLetterQueueName::fromString(str_repeat('x', 100));
|
|
expect($maxName->toString())->toBe(str_repeat('x', 100));
|
|
});
|
|
|
|
it('handles all valid characters', function () {
|
|
$validChars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-.';
|
|
$dlqName = DeadLetterQueueName::fromString($validChars);
|
|
|
|
expect($dlqName->toString())->toBe($validChars);
|
|
});
|
|
|
|
it('provides specific error messages for validation failures', function () {
|
|
// Too short
|
|
try {
|
|
DeadLetterQueueName::fromString('');
|
|
expect(false)->toBeTrue('Should have thrown exception');
|
|
} catch (InvalidDeadLetterQueueNameException $e) {
|
|
expect($e->getMessage())->toContain('too short');
|
|
}
|
|
|
|
// Too long
|
|
try {
|
|
DeadLetterQueueName::fromString(str_repeat('a', 101));
|
|
expect(false)->toBeTrue('Should have thrown exception');
|
|
} catch (InvalidDeadLetterQueueNameException $e) {
|
|
expect($e->getMessage())->toContain('too long');
|
|
}
|
|
|
|
// Invalid format
|
|
try {
|
|
DeadLetterQueueName::fromString('invalid@name');
|
|
expect(false)->toBeTrue('Should have thrown exception');
|
|
} catch (InvalidDeadLetterQueueNameException $e) {
|
|
expect($e->getMessage())->toContain('invalid format');
|
|
}
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Dead Letter Queue Mock Implementation', function () {
|
|
|
|
beforeEach(function () {
|
|
// Create a mock dead letter queue for testing
|
|
$this->mockDlq = new class {
|
|
private array $jobs = [];
|
|
private array $stats = [];
|
|
|
|
public function addFailedJob(array $jobData): void {
|
|
$this->jobs[] = $jobData;
|
|
}
|
|
|
|
public function getJobs(DeadLetterQueueName $queueName, int $limit = 100): array {
|
|
return array_slice(
|
|
array_filter($this->jobs, fn($job) => $job['dlq_name'] === $queueName->toString()),
|
|
0,
|
|
$limit
|
|
);
|
|
}
|
|
|
|
public function retryJob(string $jobId): bool {
|
|
foreach ($this->jobs as $index => $job) {
|
|
if ($job['id'] === $jobId) {
|
|
unset($this->jobs[$index]);
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
public function deleteJob(string $jobId): bool {
|
|
foreach ($this->jobs as $index => $job) {
|
|
if ($job['id'] === $jobId) {
|
|
unset($this->jobs[$index]);
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
public function clearQueue(DeadLetterQueueName $queueName): int {
|
|
$initialCount = count($this->jobs);
|
|
$this->jobs = array_filter(
|
|
$this->jobs,
|
|
fn($job) => $job['dlq_name'] !== $queueName->toString()
|
|
);
|
|
return $initialCount - count($this->jobs);
|
|
}
|
|
|
|
public function getQueueStats(DeadLetterQueueName $queueName): array {
|
|
$jobs = $this->getJobs($queueName);
|
|
return [
|
|
'queue_name' => $queueName->toString(),
|
|
'total_jobs' => count($jobs),
|
|
'avg_failed_attempts' => count($jobs) > 0 ? array_sum(array_column($jobs, 'failed_attempts')) / count($jobs) : 0,
|
|
'max_failed_attempts' => count($jobs) > 0 ? max(array_column($jobs, 'failed_attempts')) : 0,
|
|
'avg_retry_count' => count($jobs) > 0 ? array_sum(array_column($jobs, 'retry_count')) / count($jobs) : 0,
|
|
'max_retry_count' => count($jobs) > 0 ? max(array_column($jobs, 'retry_count')) : 0,
|
|
'oldest_job' => count($jobs) > 0 ? min(array_column($jobs, 'failed_at')) : null,
|
|
'newest_job' => count($jobs) > 0 ? max(array_column($jobs, 'failed_at')) : null
|
|
];
|
|
}
|
|
|
|
public function getAvailableQueues(): array {
|
|
$queueNames = array_unique(array_column($this->jobs, 'dlq_name'));
|
|
return array_map(fn($name) => DeadLetterQueueName::fromString($name), $queueNames);
|
|
}
|
|
};
|
|
});
|
|
|
|
describe('Dead Letter Queue Operations', function () {
|
|
it('can add failed jobs', function () {
|
|
$dlqName = DeadLetterQueueName::fromString('email_failed');
|
|
$jobId = JobId::generate();
|
|
|
|
$jobData = [
|
|
'id' => uniqid(),
|
|
'original_job_id' => $jobId->toString(),
|
|
'dlq_name' => $dlqName->toString(),
|
|
'original_queue' => 'email',
|
|
'job_payload' => serialize(['type' => 'email', 'data' => 'test']),
|
|
'failure_reason' => 'Connection timeout',
|
|
'exception_type' => 'TimeoutException',
|
|
'stack_trace' => 'Stack trace here...',
|
|
'failed_attempts' => 3,
|
|
'failed_at' => date('Y-m-d H:i:s'),
|
|
'retry_count' => 0
|
|
];
|
|
|
|
$this->mockDlq->addFailedJob($jobData);
|
|
|
|
$jobs = $this->mockDlq->getJobs($dlqName);
|
|
expect(count($jobs))->toBe(1);
|
|
expect($jobs[0]['original_job_id'])->toBe($jobId->toString());
|
|
});
|
|
|
|
it('can retrieve jobs by queue name', function () {
|
|
$emailDlq = DeadLetterQueueName::fromString('email_failed');
|
|
$reportDlq = DeadLetterQueueName::fromString('report_failed');
|
|
|
|
// Add jobs to different queues
|
|
$this->mockDlq->addFailedJob([
|
|
'id' => 'job1',
|
|
'original_job_id' => 'original1',
|
|
'dlq_name' => $emailDlq->toString(),
|
|
'original_queue' => 'email',
|
|
'job_payload' => 'payload1',
|
|
'failure_reason' => 'Error 1',
|
|
'exception_type' => 'Exception',
|
|
'stack_trace' => 'trace1',
|
|
'failed_attempts' => 1,
|
|
'failed_at' => date('Y-m-d H:i:s'),
|
|
'retry_count' => 0
|
|
]);
|
|
|
|
$this->mockDlq->addFailedJob([
|
|
'id' => 'job2',
|
|
'original_job_id' => 'original2',
|
|
'dlq_name' => $reportDlq->toString(),
|
|
'original_queue' => 'report',
|
|
'job_payload' => 'payload2',
|
|
'failure_reason' => 'Error 2',
|
|
'exception_type' => 'Exception',
|
|
'stack_trace' => 'trace2',
|
|
'failed_attempts' => 2,
|
|
'failed_at' => date('Y-m-d H:i:s'),
|
|
'retry_count' => 0
|
|
]);
|
|
|
|
$emailJobs = $this->mockDlq->getJobs($emailDlq);
|
|
$reportJobs = $this->mockDlq->getJobs($reportDlq);
|
|
|
|
expect(count($emailJobs))->toBe(1);
|
|
expect(count($reportJobs))->toBe(1);
|
|
expect($emailJobs[0]['id'])->toBe('job1');
|
|
expect($reportJobs[0]['id'])->toBe('job2');
|
|
});
|
|
|
|
it('can retry failed jobs', function () {
|
|
$dlqName = DeadLetterQueueName::fromString('test_failed');
|
|
$jobId = 'retry_test_job';
|
|
|
|
$this->mockDlq->addFailedJob([
|
|
'id' => $jobId,
|
|
'original_job_id' => 'original_retry',
|
|
'dlq_name' => $dlqName->toString(),
|
|
'original_queue' => 'test',
|
|
'job_payload' => 'retry_payload',
|
|
'failure_reason' => 'Temporary error',
|
|
'exception_type' => 'TemporaryException',
|
|
'stack_trace' => 'retry_trace',
|
|
'failed_attempts' => 1,
|
|
'failed_at' => date('Y-m-d H:i:s'),
|
|
'retry_count' => 0
|
|
]);
|
|
|
|
expect(count($this->mockDlq->getJobs($dlqName)))->toBe(1);
|
|
|
|
$retryResult = $this->mockDlq->retryJob($jobId);
|
|
expect($retryResult)->toBeTrue();
|
|
|
|
// Job should be removed from DLQ after retry
|
|
expect(count($this->mockDlq->getJobs($dlqName)))->toBe(0);
|
|
});
|
|
|
|
it('can delete failed jobs permanently', function () {
|
|
$dlqName = DeadLetterQueueName::fromString('delete_test');
|
|
$jobId = 'delete_test_job';
|
|
|
|
$this->mockDlq->addFailedJob([
|
|
'id' => $jobId,
|
|
'original_job_id' => 'original_delete',
|
|
'dlq_name' => $dlqName->toString(),
|
|
'original_queue' => 'delete_test',
|
|
'job_payload' => 'delete_payload',
|
|
'failure_reason' => 'Permanent error',
|
|
'exception_type' => 'PermanentException',
|
|
'stack_trace' => 'delete_trace',
|
|
'failed_attempts' => 5,
|
|
'failed_at' => date('Y-m-d H:i:s'),
|
|
'retry_count' => 0
|
|
]);
|
|
|
|
expect(count($this->mockDlq->getJobs($dlqName)))->toBe(1);
|
|
|
|
$deleteResult = $this->mockDlq->deleteJob($jobId);
|
|
expect($deleteResult)->toBeTrue();
|
|
|
|
expect(count($this->mockDlq->getJobs($dlqName)))->toBe(0);
|
|
});
|
|
|
|
it('can clear entire queue', function () {
|
|
$dlqName = DeadLetterQueueName::fromString('clear_test');
|
|
|
|
// Add multiple jobs
|
|
for ($i = 1; $i <= 5; $i++) {
|
|
$this->mockDlq->addFailedJob([
|
|
'id' => "clear_job_{$i}",
|
|
'original_job_id' => "original_{$i}",
|
|
'dlq_name' => $dlqName->toString(),
|
|
'original_queue' => 'clear_test',
|
|
'job_payload' => "payload_{$i}",
|
|
'failure_reason' => "Error {$i}",
|
|
'exception_type' => 'Exception',
|
|
'stack_trace' => "trace_{$i}",
|
|
'failed_attempts' => $i,
|
|
'failed_at' => date('Y-m-d H:i:s'),
|
|
'retry_count' => 0
|
|
]);
|
|
}
|
|
|
|
expect(count($this->mockDlq->getJobs($dlqName)))->toBe(5);
|
|
|
|
$clearedCount = $this->mockDlq->clearQueue($dlqName);
|
|
expect($clearedCount)->toBe(5);
|
|
expect(count($this->mockDlq->getJobs($dlqName)))->toBe(0);
|
|
});
|
|
});
|
|
|
|
describe('Dead Letter Queue Statistics', function () {
|
|
it('provides accurate queue statistics', function () {
|
|
$dlqName = DeadLetterQueueName::fromString('stats_test');
|
|
|
|
// Add jobs with varying statistics
|
|
$jobs = [
|
|
['failed_attempts' => 1, 'retry_count' => 0],
|
|
['failed_attempts' => 3, 'retry_count' => 1],
|
|
['failed_attempts' => 5, 'retry_count' => 2],
|
|
['failed_attempts' => 2, 'retry_count' => 0],
|
|
];
|
|
|
|
foreach ($jobs as $index => $jobStats) {
|
|
$this->mockDlq->addFailedJob([
|
|
'id' => "stats_job_{$index}",
|
|
'original_job_id' => "original_stats_{$index}",
|
|
'dlq_name' => $dlqName->toString(),
|
|
'original_queue' => 'stats_test',
|
|
'job_payload' => "payload_{$index}",
|
|
'failure_reason' => "Error {$index}",
|
|
'exception_type' => 'Exception',
|
|
'stack_trace' => "trace_{$index}",
|
|
'failed_attempts' => $jobStats['failed_attempts'],
|
|
'failed_at' => date('Y-m-d H:i:s', time() - $index * 3600), // Different times
|
|
'retry_count' => $jobStats['retry_count']
|
|
]);
|
|
}
|
|
|
|
$stats = $this->mockDlq->getQueueStats($dlqName);
|
|
|
|
expect($stats['queue_name'])->toBe($dlqName->toString());
|
|
expect($stats['total_jobs'])->toBe(4);
|
|
expect($stats['avg_failed_attempts'])->toBe(2.75); // (1+3+5+2)/4
|
|
expect($stats['max_failed_attempts'])->toBe(5);
|
|
expect($stats['avg_retry_count'])->toBe(0.75); // (0+1+2+0)/4
|
|
expect($stats['max_retry_count'])->toBe(2);
|
|
expect($stats['oldest_job'])->not->toBeNull();
|
|
expect($stats['newest_job'])->not->toBeNull();
|
|
});
|
|
|
|
it('handles empty queue statistics', function () {
|
|
$dlqName = DeadLetterQueueName::fromString('empty_stats');
|
|
|
|
$stats = $this->mockDlq->getQueueStats($dlqName);
|
|
|
|
expect($stats['total_jobs'])->toBe(0);
|
|
expect($stats['avg_failed_attempts'])->toBe(0);
|
|
expect($stats['max_failed_attempts'])->toBe(0);
|
|
expect($stats['avg_retry_count'])->toBe(0);
|
|
expect($stats['max_retry_count'])->toBe(0);
|
|
expect($stats['oldest_job'])->toBeNull();
|
|
expect($stats['newest_job'])->toBeNull();
|
|
});
|
|
|
|
it('can list available queues', function () {
|
|
$queues = [
|
|
DeadLetterQueueName::fromString('email_failed'),
|
|
DeadLetterQueueName::fromString('report_failed'),
|
|
DeadLetterQueueName::fromString('background_failed')
|
|
];
|
|
|
|
foreach ($queues as $index => $queue) {
|
|
$this->mockDlq->addFailedJob([
|
|
'id' => "queue_job_{$index}",
|
|
'original_job_id' => "original_{$index}",
|
|
'dlq_name' => $queue->toString(),
|
|
'original_queue' => 'test',
|
|
'job_payload' => "payload_{$index}",
|
|
'failure_reason' => "Error {$index}",
|
|
'exception_type' => 'Exception',
|
|
'stack_trace' => "trace_{$index}",
|
|
'failed_attempts' => 1,
|
|
'failed_at' => date('Y-m-d H:i:s'),
|
|
'retry_count' => 0
|
|
]);
|
|
}
|
|
|
|
$availableQueues = $this->mockDlq->getAvailableQueues();
|
|
|
|
expect(count($availableQueues))->toBe(3);
|
|
|
|
$queueNames = array_map(fn($q) => $q->toString(), $availableQueues);
|
|
expect($queueNames)->toContain('email_failed');
|
|
expect($queueNames)->toContain('report_failed');
|
|
expect($queueNames)->toContain('background_failed');
|
|
});
|
|
});
|
|
|
|
describe('Dead Letter Queue Error Scenarios', function () {
|
|
it('handles retry of non-existent job', function () {
|
|
$result = $this->mockDlq->retryJob('non_existent_job');
|
|
expect($result)->toBeFalse();
|
|
});
|
|
|
|
it('handles delete of non-existent job', function () {
|
|
$result = $this->mockDlq->deleteJob('non_existent_job');
|
|
expect($result)->toBeFalse();
|
|
});
|
|
|
|
it('handles clear of non-existent queue', function () {
|
|
$nonExistentQueue = DeadLetterQueueName::fromString('non_existent');
|
|
$clearedCount = $this->mockDlq->clearQueue($nonExistentQueue);
|
|
expect($clearedCount)->toBe(0);
|
|
});
|
|
|
|
it('respects job limit when retrieving jobs', function () {
|
|
$dlqName = DeadLetterQueueName::fromString('limit_test');
|
|
|
|
// Add 10 jobs
|
|
for ($i = 1; $i <= 10; $i++) {
|
|
$this->mockDlq->addFailedJob([
|
|
'id' => "limit_job_{$i}",
|
|
'original_job_id' => "original_{$i}",
|
|
'dlq_name' => $dlqName->toString(),
|
|
'original_queue' => 'limit_test',
|
|
'job_payload' => "payload_{$i}",
|
|
'failure_reason' => "Error {$i}",
|
|
'exception_type' => 'Exception',
|
|
'stack_trace' => "trace_{$i}",
|
|
'failed_attempts' => 1,
|
|
'failed_at' => date('Y-m-d H:i:s'),
|
|
'retry_count' => 0
|
|
]);
|
|
}
|
|
|
|
$limitedJobs = $this->mockDlq->getJobs($dlqName, 5);
|
|
expect(count($limitedJobs))->toBe(5);
|
|
|
|
$allJobs = $this->mockDlq->getJobs($dlqName, 100);
|
|
expect(count($allJobs))->toBe(10);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Dead Letter Queue Integration Scenarios', function () {
|
|
|
|
beforeEach(function () {
|
|
$this->testJob = new class {
|
|
public function __construct(
|
|
public string $email = 'test@example.com',
|
|
public string $subject = 'Test Email'
|
|
) {}
|
|
|
|
public function handle(): bool {
|
|
// Simulate processing that might fail
|
|
if ($this->email === 'invalid@test.com') {
|
|
throw new \Exception('Invalid email address');
|
|
}
|
|
return true;
|
|
}
|
|
};
|
|
|
|
$this->failureScenarios = [
|
|
'network_timeout' => [
|
|
'reason' => 'Network connection timeout',
|
|
'exception' => 'NetworkTimeoutException',
|
|
'retryable' => true
|
|
],
|
|
'invalid_data' => [
|
|
'reason' => 'Invalid email format',
|
|
'exception' => 'ValidationException',
|
|
'retryable' => false
|
|
],
|
|
'service_unavailable' => [
|
|
'reason' => 'External service unavailable',
|
|
'exception' => 'ServiceUnavailableException',
|
|
'retryable' => true
|
|
],
|
|
'permission_denied' => [
|
|
'reason' => 'Insufficient permissions',
|
|
'exception' => 'PermissionException',
|
|
'retryable' => false
|
|
]
|
|
];
|
|
});
|
|
|
|
it('handles different types of job failures', function () {
|
|
$emailDlq = DeadLetterQueueName::fromString('email_failed');
|
|
|
|
foreach ($this->failureScenarios as $scenarioName => $scenario) {
|
|
$jobPayload = JobPayload::create($this->testJob);
|
|
$jobId = JobId::generate();
|
|
|
|
// Simulate job failure based on scenario
|
|
$expectedRetryable = $scenario['retryable'];
|
|
|
|
expect($scenario['reason'])->toBeString();
|
|
expect($scenario['exception'])->toBeString();
|
|
expect($expectedRetryable)->toBeBool();
|
|
}
|
|
});
|
|
|
|
it('demonstrates retry strategies for different failure types', function () {
|
|
$retryableFailures = array_filter(
|
|
$this->failureScenarios,
|
|
fn($scenario) => $scenario['retryable']
|
|
);
|
|
|
|
$nonRetryableFailures = array_filter(
|
|
$this->failureScenarios,
|
|
fn($scenario) => !$scenario['retryable']
|
|
);
|
|
|
|
expect(count($retryableFailures))->toBeGreaterThan(0);
|
|
expect(count($nonRetryableFailures))->toBeGreaterThan(0);
|
|
|
|
// Retryable failures should be candidates for retry
|
|
foreach ($retryableFailures as $scenario) {
|
|
expect($scenario['retryable'])->toBeTrue();
|
|
}
|
|
|
|
// Non-retryable failures should be handled differently
|
|
foreach ($nonRetryableFailures as $scenario) {
|
|
expect($scenario['retryable'])->toBeFalse();
|
|
}
|
|
});
|
|
|
|
it('demonstrates queue-specific dead letter handling', function () {
|
|
$queueTypes = [
|
|
'email' => DeadLetterQueueName::forQueue(QueueName::fromString('email')),
|
|
'reports' => DeadLetterQueueName::forQueue(QueueName::fromString('reports')),
|
|
'background' => DeadLetterQueueName::forQueue(QueueName::fromString('background'))
|
|
];
|
|
|
|
foreach ($queueTypes as $originalQueue => $dlqName) {
|
|
expect($dlqName->toString())->toBe($originalQueue . '_failed');
|
|
}
|
|
|
|
// Each queue type should have its own DLQ
|
|
$queueNames = array_map(fn($dlq) => $dlq->toString(), $queueTypes);
|
|
$uniqueNames = array_unique($queueNames);
|
|
expect(count($uniqueNames))->toBe(count($queueTypes));
|
|
});
|
|
}); |