Skip to content
Open
Show file tree
Hide file tree
Changes from 5 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;
}
}