- 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.
365 lines
13 KiB
PHP
365 lines
13 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Framework\Core\ValueObjects\Duration;
|
|
use App\Framework\Queue\InMemoryQueue;
|
|
use App\Framework\Queue\ValueObjects\JobPayload;
|
|
use App\Framework\Queue\ValueObjects\QueuePriority;
|
|
|
|
describe('Queue Interface Basic Operations', function () {
|
|
|
|
beforeEach(function () {
|
|
$this->queue = new InMemoryQueue();
|
|
$this->testJob = new class () {
|
|
public function handle(): string
|
|
{
|
|
return 'test job executed';
|
|
}
|
|
};
|
|
});
|
|
|
|
describe('push() operation', function () {
|
|
it('can push jobs to queue', function () {
|
|
$payload = JobPayload::create($this->testJob);
|
|
|
|
$this->queue->push($payload);
|
|
|
|
expect($this->queue->size())->toBe(1);
|
|
});
|
|
|
|
it('maintains priority order when pushing multiple jobs', function () {
|
|
$lowPriorityJob = JobPayload::create($this->testJob, QueuePriority::low());
|
|
$highPriorityJob = JobPayload::create($this->testJob, QueuePriority::high());
|
|
$criticalJob = JobPayload::create($this->testJob, QueuePriority::critical());
|
|
|
|
// Push in random order
|
|
$this->queue->push($lowPriorityJob);
|
|
$this->queue->push($criticalJob);
|
|
$this->queue->push($highPriorityJob);
|
|
|
|
expect($this->queue->size())->toBe(3);
|
|
|
|
// Peek should return critical priority first
|
|
$next = $this->queue->peek();
|
|
expect($next->priority->isCritical())->toBeTrue();
|
|
});
|
|
|
|
it('accepts jobs with different configurations', function () {
|
|
$immediateJob = JobPayload::immediate($this->testJob);
|
|
$delayedJob = JobPayload::delayed($this->testJob, Duration::fromSeconds(30));
|
|
$backgroundJob = JobPayload::background($this->testJob);
|
|
$criticalJob = JobPayload::critical($this->testJob);
|
|
|
|
$this->queue->push($immediateJob);
|
|
$this->queue->push($delayedJob);
|
|
$this->queue->push($backgroundJob);
|
|
$this->queue->push($criticalJob);
|
|
|
|
expect($this->queue->size())->toBe(4);
|
|
});
|
|
});
|
|
|
|
describe('pop() operation', function () {
|
|
it('returns null when queue is empty', function () {
|
|
expect($this->queue->pop())->toBeNull();
|
|
});
|
|
|
|
it('returns and removes highest priority job first', function () {
|
|
$lowJob = JobPayload::create($this->testJob, QueuePriority::low());
|
|
$highJob = JobPayload::create($this->testJob, QueuePriority::high());
|
|
|
|
$this->queue->push($lowJob);
|
|
$this->queue->push($highJob);
|
|
|
|
$popped = $this->queue->pop();
|
|
expect($popped->priority->isHigh())->toBeTrue();
|
|
expect($this->queue->size())->toBe(1);
|
|
|
|
$remaining = $this->queue->pop();
|
|
expect($remaining->priority->isLow())->toBeTrue();
|
|
expect($this->queue->size())->toBe(0);
|
|
});
|
|
|
|
it('processes FIFO for same priority jobs', function () {
|
|
$job1 = new class () {
|
|
public $id = 1;
|
|
};
|
|
$job2 = new class () {
|
|
public $id = 2;
|
|
};
|
|
|
|
$payload1 = JobPayload::create($job1, QueuePriority::normal());
|
|
$payload2 = JobPayload::create($job2, QueuePriority::normal());
|
|
|
|
$this->queue->push($payload1);
|
|
$this->queue->push($payload2);
|
|
|
|
$first = $this->queue->pop();
|
|
expect($first->job->id)->toBe(1);
|
|
|
|
$second = $this->queue->pop();
|
|
expect($second->job->id)->toBe(2);
|
|
});
|
|
});
|
|
|
|
describe('peek() operation', function () {
|
|
it('returns null when queue is empty', function () {
|
|
expect($this->queue->peek())->toBeNull();
|
|
});
|
|
|
|
it('returns next job without removing it', function () {
|
|
$payload = JobPayload::create($this->testJob);
|
|
$this->queue->push($payload);
|
|
|
|
$peeked = $this->queue->peek();
|
|
expect($peeked)->not->toBeNull();
|
|
expect($this->queue->size())->toBe(1);
|
|
|
|
// Should return same job when peeked again
|
|
$peekedAgain = $this->queue->peek();
|
|
expect($peekedAgain)->toBe($peeked);
|
|
});
|
|
|
|
it('shows highest priority job', function () {
|
|
$normalJob = JobPayload::create($this->testJob, QueuePriority::normal());
|
|
$criticalJob = JobPayload::create($this->testJob, QueuePriority::critical());
|
|
|
|
$this->queue->push($normalJob);
|
|
$this->queue->push($criticalJob);
|
|
|
|
$peeked = $this->queue->peek();
|
|
expect($peeked->priority->isCritical())->toBeTrue();
|
|
});
|
|
});
|
|
|
|
describe('size() operation', function () {
|
|
it('returns 0 for empty queue', function () {
|
|
expect($this->queue->size())->toBe(0);
|
|
});
|
|
|
|
it('tracks size correctly as jobs are added and removed', function () {
|
|
expect($this->queue->size())->toBe(0);
|
|
|
|
$this->queue->push(JobPayload::create($this->testJob));
|
|
expect($this->queue->size())->toBe(1);
|
|
|
|
$this->queue->push(JobPayload::create($this->testJob));
|
|
expect($this->queue->size())->toBe(2);
|
|
|
|
$this->queue->pop();
|
|
expect($this->queue->size())->toBe(1);
|
|
|
|
$this->queue->pop();
|
|
expect($this->queue->size())->toBe(0);
|
|
});
|
|
});
|
|
|
|
describe('clear() operation', function () {
|
|
it('returns 0 when clearing empty queue', function () {
|
|
expect($this->queue->clear())->toBe(0);
|
|
});
|
|
|
|
it('removes all jobs and returns count', function () {
|
|
$this->queue->push(JobPayload::create($this->testJob));
|
|
$this->queue->push(JobPayload::create($this->testJob));
|
|
$this->queue->push(JobPayload::create($this->testJob));
|
|
|
|
expect($this->queue->size())->toBe(3);
|
|
|
|
$cleared = $this->queue->clear();
|
|
expect($cleared)->toBe(3);
|
|
expect($this->queue->size())->toBe(0);
|
|
});
|
|
|
|
it('queue is usable after clearing', function () {
|
|
$this->queue->push(JobPayload::create($this->testJob));
|
|
$this->queue->clear();
|
|
|
|
// Should be able to add new jobs
|
|
$this->queue->push(JobPayload::create($this->testJob));
|
|
expect($this->queue->size())->toBe(1);
|
|
});
|
|
});
|
|
|
|
describe('getStats() operation', function () {
|
|
it('returns basic stats for empty queue', function () {
|
|
$stats = $this->queue->getStats();
|
|
|
|
expect($stats)->toHaveKey('size');
|
|
expect($stats['size'])->toBe(0);
|
|
expect($stats)->toHaveKey('priority_breakdown');
|
|
expect($stats['priority_breakdown'])->toBe([]);
|
|
});
|
|
|
|
it('provides priority breakdown for populated queue', function () {
|
|
$this->queue->push(JobPayload::create($this->testJob, QueuePriority::high()));
|
|
$this->queue->push(JobPayload::create($this->testJob, QueuePriority::high()));
|
|
$this->queue->push(JobPayload::create($this->testJob, QueuePriority::normal()));
|
|
$this->queue->push(JobPayload::create($this->testJob, QueuePriority::low()));
|
|
|
|
$stats = $this->queue->getStats();
|
|
|
|
expect($stats['size'])->toBe(4);
|
|
expect($stats['priority_breakdown']['high'])->toBe(2);
|
|
expect($stats['priority_breakdown']['normal'])->toBe(1);
|
|
expect($stats['priority_breakdown']['low'])->toBe(1);
|
|
});
|
|
|
|
it('updates stats as queue changes', function () {
|
|
$this->queue->push(JobPayload::create($this->testJob, QueuePriority::critical()));
|
|
$this->queue->push(JobPayload::create($this->testJob, QueuePriority::normal()));
|
|
|
|
$stats = $this->queue->getStats();
|
|
expect($stats['size'])->toBe(2);
|
|
expect($stats['priority_breakdown']['critical'])->toBe(1);
|
|
|
|
// Remove one job
|
|
$this->queue->pop();
|
|
$updatedStats = $this->queue->getStats();
|
|
expect($updatedStats['size'])->toBe(1);
|
|
expect($updatedStats['priority_breakdown']['critical'])->toBe(0);
|
|
expect($updatedStats['priority_breakdown']['normal'])->toBe(1);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Queue Priority Processing', function () {
|
|
|
|
beforeEach(function () {
|
|
$this->queue = new InMemoryQueue();
|
|
});
|
|
|
|
it('processes jobs in correct priority order', function () {
|
|
$jobs = [];
|
|
|
|
// Create jobs with different priorities
|
|
$jobs['low'] = JobPayload::create(new class () {
|
|
public $type = 'low';
|
|
}, QueuePriority::low());
|
|
$jobs['deferred'] = JobPayload::create(new class () {
|
|
public $type = 'deferred';
|
|
}, QueuePriority::deferred());
|
|
$jobs['normal'] = JobPayload::create(new class () {
|
|
public $type = 'normal';
|
|
}, QueuePriority::normal());
|
|
$jobs['high'] = JobPayload::create(new class () {
|
|
public $type = 'high';
|
|
}, QueuePriority::high());
|
|
$jobs['critical'] = JobPayload::create(new class () {
|
|
public $type = 'critical';
|
|
}, QueuePriority::critical());
|
|
|
|
// Push in random order
|
|
$this->queue->push($jobs['normal']);
|
|
$this->queue->push($jobs['deferred']);
|
|
$this->queue->push($jobs['critical']);
|
|
$this->queue->push($jobs['low']);
|
|
$this->queue->push($jobs['high']);
|
|
|
|
// Pop should return in priority order
|
|
$order = [];
|
|
while (($job = $this->queue->pop()) !== null) {
|
|
$order[] = $job->job->type;
|
|
}
|
|
|
|
expect($order)->toBe(['critical', 'high', 'normal', 'low', 'deferred']);
|
|
});
|
|
|
|
it('handles custom priority values correctly', function () {
|
|
$customHigh = JobPayload::create(new class () {
|
|
public $id = 'custom_high';
|
|
}, new QueuePriority(500));
|
|
$customLow = JobPayload::create(new class () {
|
|
public $id = 'custom_low';
|
|
}, new QueuePriority(-50));
|
|
$standardHigh = JobPayload::create(new class () {
|
|
public $id = 'standard_high';
|
|
}, QueuePriority::high());
|
|
|
|
$this->queue->push($customLow);
|
|
$this->queue->push($standardHigh);
|
|
$this->queue->push($customHigh);
|
|
|
|
$first = $this->queue->pop();
|
|
expect($first->job->id)->toBe('custom_high'); // 500 priority
|
|
|
|
$second = $this->queue->pop();
|
|
expect($second->job->id)->toBe('standard_high'); // 100 priority
|
|
|
|
$third = $this->queue->pop();
|
|
expect($third->job->id)->toBe('custom_low'); // -50 priority
|
|
});
|
|
});
|
|
|
|
describe('Queue Edge Cases', function () {
|
|
|
|
beforeEach(function () {
|
|
$this->queue = new InMemoryQueue();
|
|
});
|
|
|
|
it('handles many operations on empty queue gracefully', function () {
|
|
expect($this->queue->pop())->toBeNull();
|
|
expect($this->queue->pop())->toBeNull();
|
|
expect($this->queue->peek())->toBeNull();
|
|
expect($this->queue->peek())->toBeNull();
|
|
expect($this->queue->size())->toBe(0);
|
|
expect($this->queue->clear())->toBe(0);
|
|
expect($this->queue->clear())->toBe(0);
|
|
});
|
|
|
|
it('maintains integrity after mixed operations', function () {
|
|
$job = new class () {
|
|
public $data = 'test';
|
|
};
|
|
|
|
// Complex sequence of operations
|
|
$this->queue->push(JobPayload::create($job));
|
|
expect($this->queue->size())->toBe(1);
|
|
|
|
$peeked = $this->queue->peek();
|
|
expect($peeked->job->data)->toBe('test');
|
|
expect($this->queue->size())->toBe(1);
|
|
|
|
$popped = $this->queue->pop();
|
|
expect($popped->job->data)->toBe('test');
|
|
expect($this->queue->size())->toBe(0);
|
|
|
|
expect($this->queue->peek())->toBeNull();
|
|
expect($this->queue->pop())->toBeNull();
|
|
|
|
// Add more after emptying
|
|
$this->queue->push(JobPayload::create($job));
|
|
expect($this->queue->size())->toBe(1);
|
|
});
|
|
|
|
it('handles large number of jobs efficiently', function () {
|
|
$start = microtime(true);
|
|
|
|
// Add 1000 jobs
|
|
for ($i = 0; $i < 1000; $i++) {
|
|
$job = new class () {
|
|
public function __construct(public int $id)
|
|
{
|
|
}
|
|
};
|
|
$payload = JobPayload::create(new $job($i), QueuePriority::normal());
|
|
$this->queue->push($payload);
|
|
}
|
|
|
|
expect($this->queue->size())->toBe(1000);
|
|
|
|
// Process all jobs
|
|
$processed = 0;
|
|
while ($this->queue->pop() !== null) {
|
|
$processed++;
|
|
}
|
|
|
|
expect($processed)->toBe(1000);
|
|
expect($this->queue->size())->toBe(0);
|
|
|
|
$elapsed = microtime(true) - $start;
|
|
expect($elapsed)->toBeLessThan(1.0); // Should complete within 1 second
|
|
});
|
|
});
|