diff --git a/.changeset/socket-retry-state.md b/.changeset/socket-retry-state.md new file mode 100644 index 0000000..7bab3e0 --- /dev/null +++ b/.changeset/socket-retry-state.md @@ -0,0 +1,5 @@ +--- +"posthog-php": patch +--- + +Resend the complete batch when the Socket consumer retries a request. diff --git a/lib/Consumer/Socket.php b/lib/Consumer/Socket.php index 52cecb7..f65970b 100644 --- a/lib/Consumer/Socket.php +++ b/lib/Consumer/Socket.php @@ -113,14 +113,15 @@ private function createSocket() */ private function makeRequest($socket, $req, $retry = true) { - $bytes_written = 0; $bytes_total = strlen($req); - $closed = false; // Retries with exponential backoff until success $backoff = 100; // Set initial waiting time to 100ms while (true) { + $bytes_written = 0; + $closed = false; + // Send request to server while (!$closed && $bytes_written < $bytes_total) { try { @@ -173,6 +174,9 @@ private function makeRequest($socket, $req, $retry = true) // Retry uploading... $backoff *= 2; $socket = $this->createSocket(); + if (false === $socket) { + break; + } } return false; diff --git a/sdk_compliance_adapter/README.md b/sdk_compliance_adapter/README.md index 1c4e30b..a5b2f1e 100644 --- a/sdk_compliance_adapter/README.md +++ b/sdk_compliance_adapter/README.md @@ -68,8 +68,9 @@ not SDK shutdown certification; `/flush` always calls the real public SDK method ## Known failures and applicability -LibCurl is expected to pass all 47 selected tests. Socket and foreground ForkCurl -each expose these 11 existing capture failures: +LibCurl is expected to pass all 47 selected tests. Foreground ForkCurl exposes +these 11 capture failures. The last Socket report also contains these failures; +it predates the Socket write-state repair: - `capture.retry_behavior.retries_on_503` - `capture.retry_behavior.retries_on_500` @@ -83,8 +84,11 @@ each expose these 11 existing capture failures: - `capture.deduplication.preserves_uuid_and_timestamp_on_retry` - `capture.deduplication.preserves_uuid_and_timestamp_on_batch_retry` -Socket does not reset its write state on retry, treats 408 as terminal, and ignores -Retry-After. ForkCurl does not retry HTTP rejection and reports a successful curl +Socket now resends the complete request on each attempt. Native regression tests +cover 503 and 429 followed by 200, stable request bodies, and queue drain. The full +Socket compliance suite was not rerun for that repair. Socket still treats 408 as +terminal and ignores Retry-After. ForkCurl does not retry HTTP rejection and +reports a successful curl process exit as successful delivery even for HTTP errors. The adapter exposes these behaviors rather than retrying on the SDK's behalf. diff --git a/test/ConsumerSocketTest.php b/test/ConsumerSocketTest.php index 38479ce..3fc2903 100644 --- a/test/ConsumerSocketTest.php +++ b/test/ConsumerSocketTest.php @@ -2,6 +2,7 @@ namespace PostHog\Test; +use PHPUnit\Framework\Attributes\DataProvider; use RuntimeException; class ConsumerSocketTest extends ConsumerTransportTestCase @@ -11,6 +12,35 @@ protected function consumer(): string return 'socket'; } + public static function retryableStatuses(): array + { + return ['server failure' => [503], 'rate limit' => [429]]; + } + + #[DataProvider('retryableStatuses')] + public function testRetrySendsTheCompleteBatch(int $status): void + { + $this->server->stop(); + $this->server = new LocalHttpServer([['status' => $status], ['status' => 200]]); + $client = $this->client(['maximum_backoff_duration' => 200]); + $previousTimeout = ini_set('default_socket_timeout', '1'); + + try { + self::assertTrue($client->capture(['distinctId' => 'some-user', 'event' => 'retried event'])); + self::assertTrue($client->flush()); + $requests = $this->server->requests(); + self::assertCount(2, $requests); + self::assertSame('POST /batch/ HTTP/1.1', $requests[1]['requestLine']); + self::assertSame($requests[0]['body'], $requests[1]['body']); + $payload = json_decode($requests[1]['body'], true, 512, JSON_THROW_ON_ERROR); + self::assertSame('retried event', $payload['batch'][0]['event']); + self::assertTrue($client->flush()); + self::assertCount(2, $this->server->requests()); + } finally { + ini_set('default_socket_timeout', $previousTimeout); + } + } + public function testShortTimeout(): void { $client = $this->client(['timeout' => 0.01]);