From f36b4d5a6304ec91f18bd0a165ef5f8379d7e5dc Mon Sep 17 00:00:00 2001 From: pauldambra Date: Fri, 16 Oct 2026 15:44:55 +0100 Subject: [PATCH] fix: resend complete batches on socket retries Generated-By: PostHog Desktop Task-Id: 5681be44-4986-48db-b140-1bda88f75f96 --- .changeset/socket-retry-state.md | 5 +++++ lib/Consumer/Socket.php | 8 ++++++-- sdk_compliance_adapter/README.md | 12 ++++++++---- test/ConsumerSocketTest.php | 30 ++++++++++++++++++++++++++++++ 4 files changed, 49 insertions(+), 6 deletions(-) create mode 100644 .changeset/socket-retry-state.md diff --git a/.changeset/socket-retry-state.md b/.changeset/socket-retry-state.md new file mode 100644 index 00000000..7bab3e01 --- /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 52cecb7c..f65970b7 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 1c4e30b4..a5b2f1e8 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 38479cee..3fc29031 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]);