Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion lib/Client.php
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ public function __construct(
false,
$options["debug"] ?? false,
null,
(int) ($options['timeout'] ?? 10000)
(int) ($options['timeout'] ?? HttpClient::DEFAULT_CURL_TIMEOUT)
);
$this->featureFlagsRequestTimeout = (int) ($options['feature_flag_request_timeout_ms'] ?? 3000);
$this->featureFlags = [];
Expand Down
32 changes: 29 additions & 3 deletions lib/Consumer/ForkCurl.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace PostHog\Consumer;

use PostHog\HttpClient;
use PostHog\QueueConsumer;

class ForkCurl extends QueueConsumer
Expand Down Expand Up @@ -32,6 +33,25 @@ public function getConsumer()
return $this->type;
}

/**
* Converts the configured timeout from milliseconds to integer seconds for curl.
* @return int|null Integer seconds or null for unlimited.
*/
private function timeoutSeconds(): ?int
{
$ms = isset($this->options['timeout']) ? (int)$this->options['timeout'] : HttpClient::DEFAULT_CURL_TIMEOUT;
if ($ms <= 0) {
return null;
}
$seconds = (int)ceil($ms / 1000);
return max(1, $seconds);
}

protected function runCommand(string $cmd, ?array &$output, ?int &$exit): void
{
exec($cmd, $output, $exit);
}

/**
* Make an async request to our API. Fork a curl process, immediately send
* to the API. If debug is enabled, we wait for the response.
Expand All @@ -58,7 +78,7 @@ public function flushBatch($messages)
// Compress request to file
$tmpfname = tempnam("/tmp", "forkcurl_");
$cmd2 = "echo " . $payload . " | gzip > " . $tmpfname;
exec($cmd2, $output, $exit);
$this->runCommand($cmd2, $output, $exit);

if (0 != $exit) {
$this->handleError($exit, $output);
Expand Down Expand Up @@ -89,11 +109,17 @@ public function flushBatch($messages)
$libVersion = $messages[0]['library_version'];
$cmd .= " -H 'User-Agent: $libName/$libVersion'";

// Timeout
$seconds = $this->timeoutSeconds();
if ($seconds !== null) {
$cmd .= " --max-time $seconds --connect-timeout $seconds";
}

if (!$this->debug()) {
$cmd .= " > /dev/null 2>&1 &";
}

exec($cmd, $output, $exit);
$this->runCommand($cmd, $output, $exit);

if (0 != $exit) {
$this->handleError($exit, $output);
Expand Down
3 changes: 2 additions & 1 deletion lib/Consumer/LibCurl.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ public function __construct($apiKey, $options = [], ?HttpClient $httpClient = nu
$this->maximum_backoff_duration,
$this->compress_request,
$this->debug(),
$this->options['error_handler'] ?? null
$this->options['error_handler'] ?? null,
$this->options['timeout'] ?? HttpClient::DEFAULT_CURL_TIMEOUT
);
}

Expand Down
4 changes: 3 additions & 1 deletion lib/HttpClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

class HttpClient
{
public const DEFAULT_CURL_TIMEOUT = 10000;

/**
* @var string
*/
Expand Down Expand Up @@ -47,7 +49,7 @@ public function __construct(
bool $compressRequests = false,
bool $debug = false,
?Closure $errorHandler = null,
int $curlTimeoutMilliseconds = 10000
int $curlTimeoutMilliseconds = self::DEFAULT_CURL_TIMEOUT
) {
$this->host = $host;
$this->useSsl = $useSsl;
Expand Down
67 changes: 67 additions & 0 deletions test/ConsumerForkCurlTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use PHPUnit\Framework\TestCase;
use PostHog\Client;
use ReflectionClass;

class ConsumerForkCurlTest extends TestCase
{
Expand Down Expand Up @@ -60,4 +61,70 @@ public function testAlias(): void
)
);
}

public function testConfigurePositiveTimeout(): void
{
$consumer = new MockedForkCurlConsumer(
'test_api_key',
array(
'consumer' => 'fork_curl',
'debug' => true,
'timeout' => 1500,
)
);

$rcClient = new ReflectionClass(Client::class);
$prop = $rcClient->getProperty('consumer');
$prop->setAccessible(true);
$prop->setValue($this->client, $consumer);

self::assertTrue(
$this->client->capture(
array(
'distinctId' => 'some-user',
'event' => "PHP Fork Curl'd\" Event",
),
)
);

$this->client->flush();

self::assertNotEmpty($consumer->commands);
$cmd = end($consumer->commands);
self::assertStringContainsString('--max-time 2', $cmd);
self::assertStringContainsString('--connect-timeout 2', $cmd);
}

public function testConfigureUnlimitedTimeout(): void
{
$consumer = new MockedForkCurlConsumer(
'test_api_key',
array(
'consumer' => 'fork_curl',
'debug' => true,
'timeout' => 0,
)
);

$rcClient = new ReflectionClass(Client::class);
$prop = $rcClient->getProperty('consumer');
$prop->setAccessible(true);
$prop->setValue($this->client, $consumer);

self::assertTrue(
$this->client->capture(
array(
'distinctId' => 'some-user',
'event' => "PHP Fork Curl'd\" Event",
),
)
);

$this->client->flush();

self::assertNotEmpty($consumer->commands);
$cmd = end($consumer->commands);
self::assertStringNotContainsString('max-time', $cmd);
self::assertStringNotContainsString('connect-timeout', $cmd);
}
}
49 changes: 49 additions & 0 deletions test/ConsumerLibCurlTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

use PHPUnit\Framework\TestCase;
use PostHog\Client;
use PostHog\Consumer\LibCurl;
use ReflectionClass;

class ConsumerLibCurlTest extends TestCase
{
Expand All @@ -21,6 +23,53 @@ public function setUp(): void
);
}

public function testApplyConstructOptionsToHttpClient()
{
$client = new Client(
'test_api_key',
array(
'consumer' => 'lib_curl',
'ssl' => false,
'maximum_backoff_duration' => 5000,
'compress_request' => true,
'debug' => true,
'timeout' => 1234,
)
);

$rcClient = new ReflectionClass($client);
$consumerProp = $rcClient->getProperty('consumer');
$consumerProp->setAccessible(true);
$consumer = $consumerProp->getValue($client);

$this->assertInstanceOf(LibCurl::class, $consumer);

$rcConsumer = new ReflectionClass($consumer);
$httpProp = $rcConsumer->getProperty('httpClient');
$httpProp->setAccessible(true);
$httpClient = $httpProp->getValue($consumer);

$rcHttp = new ReflectionClass($httpClient);

$expectedValues = array(
'useSsl' => false,
'maximumBackoffDuration' => 5000,
'compressRequests' => true,
'debug' => true,
'errorHandler' => null,
'curlTimeoutMilliseconds' => 1234,
);

foreach ($expectedValues as $name => $expected) {
self::assertTrue($rcHttp->hasProperty($name));

$prop = $rcHttp->getProperty($name);
$prop->setAccessible(true);
$actual = $prop->getValue($httpClient);
self::assertSame($expected, $actual);
}
}

public function testCapture(): void
{
self::assertTrue(
Expand Down
18 changes: 18 additions & 0 deletions test/MockedForkCurlConsumer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

namespace PostHog\Test;

use PostHog\Consumer\ForkCurl;

class MockedForkCurlConsumer extends ForkCurl
{
public array $commands = [];
public int $fakeExit = 0;

protected function runCommand(string $cmd, ?array &$output, ?int &$exit): void
{
$this->commands[] = $cmd;
$output = [];
$exit = $this->fakeExit;
}
}