fix(x): stop chunked video upload from exhausting memory
The APPEND loop sent each 1MB segment without releasing it: the chunk plus its multipart copies and the Guzzle/retry object graph were kept alive by reference cycles the GC doesn't reclaim mid-loop, so memory grew ~4MB per segment. A 49MB video peaked at ~242MB locally and crossed the 512MB limit in production. Freeing the segment and forcing cycle collection each iteration keeps memory flat (~50MB regardless of video size). Also harden socialHttp()'s retry predicate: it read $exception->response directly, which fatals on exceptions without a response (e.g. a ConnectionException during a long upload), masking the real error.
This commit is contained in:
parent
973fc604a2
commit
5b065ce121
3 changed files with 56 additions and 1 deletions
|
|
@ -32,7 +32,7 @@ protected function socialHttp(): PendingRequest
|
|||
return Http::retry(
|
||||
times: 3,
|
||||
sleepMilliseconds: 5000,
|
||||
when: fn ($exception, $request) => $exception->response?->status() === 429,
|
||||
when: fn ($exception, $request) => ($exception->response ?? null)?->status() === 429,
|
||||
throw: false,
|
||||
)->timeout(120);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -236,6 +236,12 @@ private function chunkedUpload(string $tempFile, int $totalBytes, string $mimeTy
|
|||
}
|
||||
|
||||
$index++;
|
||||
|
||||
// The HTTP client/retry wrapper keeps each segment's body alive
|
||||
// through reference cycles, so without forcing collection a large
|
||||
// video accumulates its full size in memory and exhausts the limit.
|
||||
unset($chunk, $appendResponse);
|
||||
gc_collect_cycles();
|
||||
}
|
||||
} finally {
|
||||
fclose($handle);
|
||||
|
|
|
|||
|
|
@ -304,3 +304,52 @@
|
|||
Http::assertSent(fn ($request) => str_contains($request->url(), '/finalize'));
|
||||
Http::assertSent(fn ($request) => str_contains($request->url(), '/2/tweets'));
|
||||
});
|
||||
|
||||
test('x publisher uploads a large video in sequential 1MB segments', function () {
|
||||
$this->post->update([
|
||||
'media' => [
|
||||
[
|
||||
'id' => 'test-media-video',
|
||||
'path' => 'media/2026-01/big-video.mp4',
|
||||
'url' => 'https://example.com/media/2026-01/big-video.mp4',
|
||||
'mime_type' => 'video/mp4',
|
||||
'original_filename' => 'big-video.mp4',
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$appendCount = 0;
|
||||
|
||||
Http::fake(function ($request) use (&$appendCount) {
|
||||
$url = $request->url();
|
||||
|
||||
if (str_contains($url, '/2/media/upload/initialize')) {
|
||||
return Http::response(['data' => ['id' => 'media_id_999']], 200);
|
||||
}
|
||||
|
||||
if (str_contains($url, '/append')) {
|
||||
$appendCount++;
|
||||
|
||||
return Http::response(null, 204);
|
||||
}
|
||||
|
||||
if (str_contains($url, '/finalize')) {
|
||||
return Http::response(['data' => ['id' => 'media_id_999']], 200);
|
||||
}
|
||||
|
||||
if (str_contains($url, '/2/media/')) {
|
||||
return Http::response(['processing_info' => ['state' => 'succeeded']], 200);
|
||||
}
|
||||
|
||||
if (str_contains($url, '/2/tweets')) {
|
||||
return Http::response(['data' => ['id' => '9876543210987654321', 'text' => 'Hello from X!']], 200);
|
||||
}
|
||||
|
||||
// ~2.5MB download -> at least three 1MB segments.
|
||||
return Http::response(str_repeat('x', (int) (2.5 * 1024 * 1024)), 200);
|
||||
});
|
||||
|
||||
$this->publisher->publish($this->postPlatform);
|
||||
|
||||
expect($appendCount)->toBeGreaterThan(1);
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue