- 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.
497 lines
15 KiB
PHP
497 lines
15 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Framework\HttpClient\Curl\Handle;
|
|
use App\Framework\HttpClient\Curl\HandleOption;
|
|
use App\Framework\HttpClient\Curl\Info;
|
|
use App\Framework\HttpClient\Curl\MultiHandle;
|
|
use App\Framework\HttpClient\Curl\Exception\MultiHandleException;
|
|
|
|
describe('Curl\MultiHandle', function () {
|
|
it('can be initialized empty', function () {
|
|
$multi = new MultiHandle();
|
|
|
|
expect($multi)->toBeInstanceOf(MultiHandle::class);
|
|
expect($multi->count())->toBe(0);
|
|
});
|
|
|
|
it('can be initialized with handles', function () {
|
|
$handle1 = new Handle('https://httpbin.org/get');
|
|
$handle2 = new Handle('https://httpbin.org/uuid');
|
|
|
|
$multi = new MultiHandle($handle1, $handle2);
|
|
|
|
expect($multi)->toBeInstanceOf(MultiHandle::class);
|
|
expect($multi->count())->toBe(2);
|
|
expect($multi->getHandles())->toHaveCount(2);
|
|
});
|
|
|
|
it('can add handles', function () {
|
|
$multi = new MultiHandle();
|
|
$handle = new Handle('https://httpbin.org/get');
|
|
|
|
$result = $multi->addHandle($handle);
|
|
|
|
expect($result)->toBe($multi); // Fluent API
|
|
expect($multi->count())->toBe(1);
|
|
});
|
|
|
|
it('can remove handles', function () {
|
|
$multi = new MultiHandle();
|
|
$handle = new Handle('https://httpbin.org/get');
|
|
|
|
$multi->addHandle($handle);
|
|
expect($multi->count())->toBe(1);
|
|
|
|
$multi->removeHandle($handle);
|
|
expect($multi->count())->toBe(0);
|
|
});
|
|
|
|
it('can get all attached handles', function () {
|
|
$multi = new MultiHandle();
|
|
$handle1 = new Handle('https://httpbin.org/get');
|
|
$handle2 = new Handle('https://httpbin.org/uuid');
|
|
|
|
$multi->addHandle($handle1);
|
|
$multi->addHandle($handle2);
|
|
|
|
$handles = $multi->getHandles();
|
|
|
|
expect($handles)->toHaveCount(2);
|
|
expect($handles)->toContain($handle1);
|
|
expect($handles)->toContain($handle2);
|
|
});
|
|
|
|
it('can execute parallel requests with executeAll()', function () {
|
|
$multi = new MultiHandle();
|
|
|
|
$urls = [
|
|
'https://httpbin.org/delay/1',
|
|
'https://httpbin.org/uuid',
|
|
'https://httpbin.org/get',
|
|
];
|
|
|
|
$handles = [];
|
|
foreach ($urls as $url) {
|
|
$handle = new Handle($url);
|
|
$handle->setOption(HandleOption::Timeout, 10);
|
|
$handles[] = $handle;
|
|
$multi->addHandle($handle);
|
|
}
|
|
|
|
expect($multi->count())->toBe(3);
|
|
|
|
$startTime = microtime(true);
|
|
$completed = $multi->executeAll();
|
|
$duration = microtime(true) - $startTime;
|
|
|
|
// Parallel execution should be faster than sequential (3+ seconds)
|
|
expect($duration)->toBeLessThan(2.5);
|
|
expect($completed)->toHaveCount(3);
|
|
|
|
// Verify all requests completed successfully
|
|
foreach ($handles as $handle) {
|
|
expect($handle->getInfo(Info::ResponseCode))->toBe(200);
|
|
}
|
|
});
|
|
|
|
it('executes requests in parallel not sequentially', function () {
|
|
$multi = new MultiHandle();
|
|
|
|
// Create 3 requests with 1 second delay each
|
|
$handles = [];
|
|
for ($i = 0; $i < 3; $i++) {
|
|
$handle = new Handle('https://httpbin.org/delay/1');
|
|
$handle->setOption(HandleOption::Timeout, 10);
|
|
$handles[] = $handle;
|
|
$multi->addHandle($handle);
|
|
}
|
|
|
|
$startTime = microtime(true);
|
|
$multi->executeAll();
|
|
$duration = microtime(true) - $startTime;
|
|
|
|
// If sequential: ~3 seconds
|
|
// If parallel: ~1 second
|
|
expect($duration)->toBeLessThan(2.0);
|
|
expect($duration)->toBeGreaterThan(0.8);
|
|
});
|
|
|
|
it('can handle mixed success and failure', function () {
|
|
$multi = new MultiHandle();
|
|
|
|
$handle1 = new Handle('https://httpbin.org/get');
|
|
$handle1->setOption(HandleOption::Timeout, 10);
|
|
|
|
$handle2 = new Handle('https://httpbin.org/status/404');
|
|
$handle2->setOption(HandleOption::Timeout, 10);
|
|
|
|
$handle3 = new Handle('https://httpbin.org/status/500');
|
|
$handle3->setOption(HandleOption::Timeout, 10);
|
|
|
|
$multi->addHandle($handle1);
|
|
$multi->addHandle($handle2);
|
|
$multi->addHandle($handle3);
|
|
|
|
$completed = $multi->executeAll();
|
|
|
|
expect($completed)->toHaveCount(3);
|
|
|
|
// Check individual status codes
|
|
expect($handle1->getInfo(Info::ResponseCode))->toBe(200);
|
|
expect($handle2->getInfo(Info::ResponseCode))->toBe(404);
|
|
expect($handle3->getInfo(Info::ResponseCode))->toBe(500);
|
|
});
|
|
|
|
it('can execute with manual control loop', function () {
|
|
$multi = new MultiHandle();
|
|
|
|
$handle = new Handle('https://httpbin.org/get');
|
|
$handle->setOption(HandleOption::Timeout, 10);
|
|
$multi->addHandle($handle);
|
|
|
|
$stillRunning = 0;
|
|
$iterations = 0;
|
|
|
|
do {
|
|
$multi->execute($stillRunning);
|
|
|
|
if ($stillRunning > 0) {
|
|
$multi->select();
|
|
}
|
|
|
|
$iterations++;
|
|
} while ($stillRunning > 0 && $iterations < 100);
|
|
|
|
expect($stillRunning)->toBe(0);
|
|
expect($handle->getInfo(Info::ResponseCode))->toBe(200);
|
|
});
|
|
|
|
it('can handle many parallel requests', function () {
|
|
$multi = new MultiHandle();
|
|
|
|
$numRequests = 10;
|
|
$handles = [];
|
|
|
|
for ($i = 0; $i < $numRequests; $i++) {
|
|
$handle = new Handle('https://httpbin.org/get');
|
|
$handle->setOption(HandleOption::Timeout, 10);
|
|
$handles[] = $handle;
|
|
$multi->addHandle($handle);
|
|
}
|
|
|
|
expect($multi->count())->toBe($numRequests);
|
|
|
|
$startTime = microtime(true);
|
|
$completed = $multi->executeAll();
|
|
$duration = microtime(true) - $startTime;
|
|
|
|
expect($completed)->toHaveCount($numRequests);
|
|
|
|
// Should complete much faster than sequential
|
|
expect($duration)->toBeLessThan(5.0);
|
|
|
|
// Verify all completed
|
|
foreach ($handles as $handle) {
|
|
expect($handle->getInfo(Info::ResponseCode))->toBe(200);
|
|
}
|
|
});
|
|
|
|
it('provides access to underlying resource', function () {
|
|
$multi = new MultiHandle();
|
|
|
|
$resource = $multi->getResource();
|
|
|
|
expect($resource)->toBeInstanceOf(CurlMultiHandle::class);
|
|
});
|
|
|
|
it('can set multi handle options', function () {
|
|
$multi = new MultiHandle();
|
|
|
|
// CURLMOPT_PIPELINING enable HTTP/1.1 pipelining
|
|
if (defined('CURLMOPT_PIPELINING')) {
|
|
$result = $multi->setOption(CURLMOPT_PIPELINING, 1);
|
|
expect($result)->toBe($multi); // Fluent API
|
|
}
|
|
|
|
expect($multi)->toBeInstanceOf(MultiHandle::class);
|
|
});
|
|
|
|
it('handles empty multi handle gracefully', function () {
|
|
$multi = new MultiHandle();
|
|
|
|
// Execute without any handles
|
|
$completed = $multi->executeAll();
|
|
|
|
expect($completed)->toHaveCount(0);
|
|
expect($multi->count())->toBe(0);
|
|
});
|
|
|
|
it('can add and remove handles multiple times', function () {
|
|
$multi = new MultiHandle();
|
|
$handle1 = new Handle('https://httpbin.org/get');
|
|
$handle2 = new Handle('https://httpbin.org/uuid');
|
|
|
|
$multi->addHandle($handle1);
|
|
expect($multi->count())->toBe(1);
|
|
|
|
$multi->addHandle($handle2);
|
|
expect($multi->count())->toBe(2);
|
|
|
|
$multi->removeHandle($handle1);
|
|
expect($multi->count())->toBe(1);
|
|
|
|
$multi->removeHandle($handle2);
|
|
expect($multi->count())->toBe(0);
|
|
});
|
|
|
|
it('select returns number of handles with activity', function () {
|
|
$multi = new MultiHandle();
|
|
|
|
$handle = new Handle('https://httpbin.org/delay/1');
|
|
$handle->setOption(HandleOption::Timeout, 10);
|
|
$multi->addHandle($handle);
|
|
|
|
$stillRunning = 0;
|
|
$multi->execute($stillRunning);
|
|
|
|
if ($stillRunning > 0) {
|
|
$result = $multi->select(1.0);
|
|
// Result can be -1 (error), 0 (timeout), or >0 (activity)
|
|
expect($result)->toBeGreaterThanOrEqual(-1);
|
|
}
|
|
|
|
expect($stillRunning)->toBeGreaterThanOrEqual(0);
|
|
});
|
|
|
|
it('getInfo returns transfer information', function () {
|
|
$multi = new MultiHandle();
|
|
|
|
$handle = new Handle('https://httpbin.org/get');
|
|
$handle->setOption(HandleOption::Timeout, 10);
|
|
$multi->addHandle($handle);
|
|
|
|
$stillRunning = 0;
|
|
$messagesInQueue = 0;
|
|
|
|
do {
|
|
$multi->execute($stillRunning);
|
|
|
|
if ($stillRunning > 0) {
|
|
$multi->select();
|
|
}
|
|
|
|
// Check for completed transfers
|
|
while ($info = $multi->getInfo($messagesInQueue)) {
|
|
expect($info)->toBeArray();
|
|
expect($info)->toHaveKey('handle');
|
|
expect($info)->toHaveKey('result');
|
|
}
|
|
} while ($stillRunning > 0);
|
|
|
|
expect($stillRunning)->toBe(0);
|
|
});
|
|
|
|
it('can handle POST requests in parallel', function () {
|
|
$multi = new MultiHandle();
|
|
|
|
$handles = [];
|
|
$data = ['name' => 'Test', 'timestamp' => time()];
|
|
|
|
for ($i = 0; $i < 3; $i++) {
|
|
$handle = new Handle('https://httpbin.org/post');
|
|
$handle->setOptions([
|
|
HandleOption::Post->value => true,
|
|
HandleOption::PostFields->value => json_encode($data),
|
|
HandleOption::HttpHeader->value => ['Content-Type: application/json'],
|
|
HandleOption::Timeout->value => 10,
|
|
]);
|
|
|
|
$handles[] = $handle;
|
|
$multi->addHandle($handle);
|
|
}
|
|
|
|
$completed = $multi->executeAll();
|
|
|
|
expect($completed)->toHaveCount(3);
|
|
|
|
foreach ($handles as $handle) {
|
|
expect($handle->getInfo(Info::ResponseCode))->toBe(200);
|
|
}
|
|
});
|
|
|
|
it('automatically cleans up on destruction', function () {
|
|
$multi = new MultiHandle();
|
|
|
|
$handle1 = new Handle('https://httpbin.org/get');
|
|
$handle2 = new Handle('https://httpbin.org/uuid');
|
|
|
|
$multi->addHandle($handle1);
|
|
$multi->addHandle($handle2);
|
|
|
|
expect($multi->count())->toBe(2);
|
|
|
|
unset($multi);
|
|
|
|
// If we reach here without errors, cleanup worked
|
|
expect(true)->toBeTrue();
|
|
});
|
|
|
|
it('can register onComplete callback', function () {
|
|
$multi = new MultiHandle();
|
|
$completedUrls = [];
|
|
|
|
$multi->onComplete(function(Handle $handle) use (&$completedUrls) {
|
|
$completedUrls[] = $handle->getInfo(Info::EffectiveUrl);
|
|
});
|
|
|
|
$handle1 = new Handle('https://httpbin.org/get');
|
|
$handle2 = new Handle('https://httpbin.org/uuid');
|
|
$handle1->setOption(HandleOption::Timeout, 10);
|
|
$handle2->setOption(HandleOption::Timeout, 10);
|
|
|
|
$multi->addHandle($handle1);
|
|
$multi->addHandle($handle2);
|
|
|
|
$multi->executeAll();
|
|
|
|
expect($completedUrls)->toHaveCount(2);
|
|
expect($completedUrls[0])->toContain('httpbin.org');
|
|
expect($completedUrls[1])->toContain('httpbin.org');
|
|
});
|
|
|
|
it('can register onError callback', function () {
|
|
$multi = new MultiHandle();
|
|
$errors = [];
|
|
|
|
$multi->onError(function(Handle $handle, int $errorCode) use (&$errors) {
|
|
$errors[] = [
|
|
'url' => $handle->getInfo(Info::EffectiveUrl),
|
|
'code' => $errorCode
|
|
];
|
|
});
|
|
|
|
// Create a handle that will timeout
|
|
$handle = new Handle('https://httpbin.org/delay/10');
|
|
$handle->setOption(HandleOption::Timeout, 1); // 1 second timeout
|
|
$multi->addHandle($handle);
|
|
|
|
$multi->executeAll();
|
|
|
|
expect($errors)->toHaveCount(1);
|
|
expect($errors[0]['code'])->toBe(CURLE_OPERATION_TIMEDOUT);
|
|
});
|
|
|
|
it('can register onProgress callback', function () {
|
|
$multi = new MultiHandle();
|
|
$progressUpdates = [];
|
|
|
|
$multi->onProgress(function(int $completed, int $total) use (&$progressUpdates) {
|
|
$progressUpdates[] = ['completed' => $completed, 'total' => $total];
|
|
});
|
|
|
|
$handle1 = new Handle('https://httpbin.org/get');
|
|
$handle2 = new Handle('https://httpbin.org/uuid');
|
|
$handle3 = new Handle('https://httpbin.org/delay/1');
|
|
|
|
$handle1->setOption(HandleOption::Timeout, 10);
|
|
$handle2->setOption(HandleOption::Timeout, 10);
|
|
$handle3->setOption(HandleOption::Timeout, 10);
|
|
|
|
$multi->addHandle($handle1);
|
|
$multi->addHandle($handle2);
|
|
$multi->addHandle($handle3);
|
|
|
|
$multi->executeAll();
|
|
|
|
// Should have progress updates
|
|
expect($progressUpdates)->not->toBeEmpty();
|
|
expect($progressUpdates)->toHaveCount(3);
|
|
|
|
// Final update should show all completed
|
|
$finalUpdate = end($progressUpdates);
|
|
expect($finalUpdate['completed'])->toBe(3);
|
|
expect($finalUpdate['total'])->toBe(3);
|
|
});
|
|
|
|
it('can chain callback registrations', function () {
|
|
$multi = new MultiHandle();
|
|
|
|
$result = $multi
|
|
->onComplete(fn($handle) => null)
|
|
->onError(fn($handle, $code) => null)
|
|
->onProgress(fn($completed, $total) => null);
|
|
|
|
expect($result)->toBe($multi);
|
|
});
|
|
|
|
it('triggers callbacks in correct order', function () {
|
|
$multi = new MultiHandle();
|
|
$events = [];
|
|
|
|
$multi->onComplete(function(Handle $handle) use (&$events) {
|
|
$events[] = 'complete';
|
|
});
|
|
|
|
$multi->onProgress(function(int $completed, int $total) use (&$events) {
|
|
$events[] = 'progress';
|
|
});
|
|
|
|
$handle = new Handle('https://httpbin.org/get');
|
|
$handle->setOption(HandleOption::Timeout, 10);
|
|
$multi->addHandle($handle);
|
|
|
|
$multi->executeAll();
|
|
|
|
expect($events)->toContain('complete');
|
|
expect($events)->toContain('progress');
|
|
});
|
|
});
|
|
|
|
describe('Curl\MultiHandle edge cases', function () {
|
|
it('handles timeout in parallel requests', function () {
|
|
$multi = new MultiHandle();
|
|
|
|
$handle1 = new Handle('https://httpbin.org/delay/1');
|
|
$handle1->setOption(HandleOption::Timeout, 10);
|
|
|
|
$handle2 = new Handle('https://httpbin.org/delay/20');
|
|
$handle2->setOption(HandleOption::Timeout, 2);
|
|
|
|
$multi->addHandle($handle1);
|
|
$multi->addHandle($handle2);
|
|
|
|
$completed = $multi->executeAll();
|
|
|
|
// Handle1 should succeed
|
|
expect($handle1->getInfo(Info::ResponseCode))->toBe(200);
|
|
|
|
// Handle2 should timeout (response code 0 or error)
|
|
$handle2Code = $handle2->getInfo(Info::ResponseCode);
|
|
expect($handle2Code)->toBeIn([0, 28]); // 0 = timeout, 28 = operation timeout
|
|
});
|
|
|
|
it('can reuse multi handle for multiple batches', function () {
|
|
$multi = new MultiHandle();
|
|
|
|
// First batch
|
|
$handle1 = new Handle('https://httpbin.org/get');
|
|
$handle1->setOption(HandleOption::Timeout, 10);
|
|
$multi->addHandle($handle1);
|
|
|
|
$multi->executeAll();
|
|
expect($handle1->getInfo(Info::ResponseCode))->toBe(200);
|
|
|
|
$multi->removeHandle($handle1);
|
|
|
|
// Second batch
|
|
$handle2 = new Handle('https://httpbin.org/uuid');
|
|
$handle2->setOption(HandleOption::Timeout, 10);
|
|
$multi->addHandle($handle2);
|
|
|
|
$multi->executeAll();
|
|
expect($handle2->getInfo(Info::ResponseCode))->toBe(200);
|
|
});
|
|
});
|