Skip to content

Commit baf9a88

Browse files
Merge pull request #439 from JLG-WOCFR-DEV/codex/review-code-for-anomalies-and-performance-issues
Allow remote request fallback and streamline Redis queue delays
2 parents 50faa04 + 99aa612 commit baf9a88

File tree

7 files changed

+255
-36
lines changed

7 files changed

+255
-36
lines changed

liens-morts-detector-jlg/assets/css/blc-admin-styles.css

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1418,7 +1418,13 @@
14181418
font-weight: 600;
14191419
font-size: 14px;
14201420
cursor: pointer;
1421-
outline: none;
1421+
}
1422+
1423+
.blc-scan-status__log-summary:focus,
1424+
.blc-scan-status__log-summary:focus-visible {
1425+
outline: 2px solid var(--wp-admin-theme-color, #2271b1);
1426+
outline-offset: 2px;
1427+
border-radius: 4px;
14221428
}
14231429

14241430
.blc-scan-status__log summary::-webkit-details-marker {

liens-morts-detector-jlg/includes/Scanner/QueueDrivers/RedisQueueDriver.php

Lines changed: 76 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ class RedisQueueDriver implements QueueDriverInterface
2626
/** @var string */
2727
private $queueKey;
2828

29+
/** @var string */
30+
private $delayedKey;
31+
2932
/** @var int */
3033
private $blockingTimeout;
3134

@@ -35,6 +38,7 @@ public function __construct(array $config = [])
3538
$this->port = isset($config['port']) ? (int) $config['port'] : 6379;
3639
$this->password = isset($config['password']) ? (string) $config['password'] : '';
3740
$this->queueKey = isset($config['queue']) ? (string) $config['queue'] : 'blc:scan-queue';
41+
$this->delayedKey = $this->queueKey . ':delayed';
3842
$this->blockingTimeout = isset($config['blocking_timeout']) ? (int) $config['blocking_timeout'] : 5;
3943
}
4044

@@ -58,12 +62,14 @@ public function scheduleBatch(array $job, int $delaySeconds = 0): bool
5862
return false;
5963
}
6064

65+
$availableAt = time() + max(0, $delaySeconds);
66+
6167
$payload = [
6268
'batch' => isset($job['batch']) ? (int) $job['batch'] : 0,
6369
'is_full_scan' => isset($job['is_full_scan']) ? (bool) $job['is_full_scan'] : false,
6470
'bypass_rest_window' => isset($job['bypass_rest_window']) ? (bool) $job['bypass_rest_window'] : false,
6571
'context' => isset($job['context']) && is_array($job['context']) ? $job['context'] : [],
66-
'available_at' => time() + max(0, $delaySeconds),
72+
'available_at' => $availableAt,
6773
'enqueued_at' => time(),
6874
];
6975

@@ -73,6 +79,10 @@ public function scheduleBatch(array $job, int $delaySeconds = 0): bool
7379
}
7480

7581
try {
82+
if ($delaySeconds > 0) {
83+
return (bool) $this->client->zAdd($this->delayedKey, $availableAt, $encoded);
84+
}
85+
7686
return (bool) $this->client->rPush($this->queueKey, $encoded);
7787
} catch (\RedisException $exception) {
7888
$this->connected = false;
@@ -86,6 +96,8 @@ public function receiveBatch(): ?array
8696
return null;
8797
}
8898

99+
$this->releaseDueDelayedJobs();
100+
89101
try {
90102
$result = $this->client->brPop([$this->queueKey], max(1, $this->blockingTimeout));
91103
} catch (\RedisException $exception) {
@@ -104,16 +116,7 @@ public function receiveBatch(): ?array
104116

105117
$availableAt = isset($payload['available_at']) ? (int) $payload['available_at'] : 0;
106118
if ($availableAt > time()) {
107-
// Not ready yet: requeue at the end and wait.
108-
try {
109-
$this->client->rPush($this->queueKey, $result[1]);
110-
} catch (\RedisException $exception) {
111-
$this->connected = false;
112-
}
113-
114-
$sleepDuration = min(5, max(1, $availableAt - time()));
115-
sleep($sleepDuration);
116-
119+
$this->storeDelayedJob($availableAt, $result[1]);
117120
return null;
118121
}
119122

@@ -215,5 +218,67 @@ private function decode($payload)
215218

216219
return $decoded;
217220
}
221+
222+
private function releaseDueDelayedJobs(): void
223+
{
224+
if (!$this->client instanceof \Redis) {
225+
return;
226+
}
227+
228+
try {
229+
$due = $this->client->zRangeByScore($this->delayedKey, '-inf', time());
230+
} catch (\RedisException $exception) {
231+
$this->connected = false;
232+
return;
233+
}
234+
235+
if (!is_array($due) || $due === []) {
236+
return;
237+
}
238+
239+
try {
240+
$transactionStarted = $this->client->multi();
241+
242+
if ($transactionStarted === false) {
243+
foreach ($due as $encoded) {
244+
$this->client->zRem($this->delayedKey, $encoded);
245+
}
246+
foreach (array_reverse($due) as $encoded) {
247+
$this->client->rPush($this->queueKey, $encoded);
248+
}
249+
250+
return;
251+
}
252+
253+
foreach ($due as $encoded) {
254+
$this->client->zRem($this->delayedKey, $encoded);
255+
}
256+
foreach (array_reverse($due) as $encoded) {
257+
$this->client->rPush($this->queueKey, $encoded);
258+
}
259+
$this->client->exec();
260+
} catch (\RedisException $exception) {
261+
$this->connected = false;
262+
263+
try {
264+
$this->client->discard();
265+
} catch (\RedisException $discardException) {
266+
// Ignore discard failures.
267+
}
268+
}
269+
}
270+
271+
private function storeDelayedJob(int $availableAt, string $encodedPayload): void
272+
{
273+
if (!$this->client instanceof \Redis) {
274+
return;
275+
}
276+
277+
try {
278+
$this->client->zAdd($this->delayedKey, $availableAt, $encodedPayload);
279+
} catch (\RedisException $exception) {
280+
$this->connected = false;
281+
}
282+
}
218283
}
219284

liens-morts-detector-jlg/includes/Scanner/RemoteRequestClient.php

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -228,10 +228,22 @@ private function dispatchRequest($method, $url, array $args)
228228
$this->lastRequestAt = microtime(true);
229229

230230
if ($method === 'head') {
231-
return \wp_safe_remote_head($url, $args);
231+
$response = \wp_safe_remote_head($url, $args);
232+
233+
if ($response instanceof WP_Error && $response->get_error_code() === 'http_request_not_allowed') {
234+
$response = \wp_remote_head($url, $args);
235+
}
236+
237+
return $response;
238+
}
239+
240+
$response = \wp_safe_remote_get($url, $args);
241+
242+
if ($response instanceof WP_Error && $response->get_error_code() === 'http_request_not_allowed') {
243+
$response = \wp_remote_get($url, $args);
232244
}
233245

234-
return \wp_safe_remote_get($url, $args);
246+
return $response;
235247
}
236248

237249
/**

liens-morts-detector-jlg/includes/blc-s3-exports.php

Lines changed: 96 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -505,12 +505,9 @@ function blc_s3_put_object(array $settings, $object_key, $file_path)
505505

506506
$headers['Content-Type'] = 'text/csv';
507507

508-
$body = file_get_contents($file_path);
509-
if ($body === false) {
510-
return new \WP_Error(
511-
'blc_s3_file_unreadable',
512-
__('Unable to open the generated CSV for S3 export.', 'liens-morts-detector-jlg')
513-
);
508+
$fileSize = @filesize($file_path);
509+
if (is_int($fileSize) && $fileSize >= 0) {
510+
$headers['Content-Length'] = (string) $fileSize;
514511
}
515512

516513
$timeout = 20;
@@ -521,19 +518,100 @@ function blc_s3_put_object(array $settings, $object_key, $file_path)
521518
}
522519
}
523520

524-
if (!function_exists('wp_remote_request')) {
525-
return new \WP_Error(
526-
'blc_s3_http_unavailable',
527-
__('The WordPress HTTP API is unavailable for S3 requests.', 'liens-morts-detector-jlg')
528-
);
529-
}
521+
$canStreamUpload = class_exists('\\Requests_Transport_cURL')
522+
&& class_exists('\\Requests_Hooks')
523+
&& class_exists('\\Requests')
524+
&& function_exists('curl_init');
530525

531-
$response = wp_remote_request($url, [
532-
'method' => 'PUT',
533-
'headers' => $headers,
534-
'body' => $body,
535-
'timeout' => $timeout,
536-
]);
526+
if ($canStreamUpload) {
527+
$handle = fopen($file_path, 'rb');
528+
if ($handle === false) {
529+
return new \WP_Error(
530+
'blc_s3_file_unreadable',
531+
__('Unable to open the generated CSV for S3 export.', 'liens-morts-detector-jlg')
532+
);
533+
}
534+
535+
$hooks = new \Requests_Hooks();
536+
$hooks->register('curl.before_send', function ($curlHandle) use ($handle, $fileSize) {
537+
curl_setopt($curlHandle, CURLOPT_UPLOAD, true);
538+
curl_setopt($curlHandle, CURLOPT_INFILE, $handle);
539+
540+
if (is_int($fileSize) && $fileSize >= 0) {
541+
curl_setopt($curlHandle, CURLOPT_INFILESIZE, $fileSize);
542+
}
543+
});
544+
545+
try {
546+
$requestsResponse = \Requests::request(
547+
$url,
548+
$headers,
549+
null,
550+
'PUT',
551+
[
552+
'timeout' => $timeout,
553+
'hooks' => $hooks,
554+
'transport' => 'Requests_Transport_cURL',
555+
'blocking' => true,
556+
]
557+
);
558+
} catch (\Exception $exception) {
559+
fclose($handle);
560+
561+
return new \WP_Error(
562+
'blc_s3_http_error',
563+
sprintf(
564+
__('S3 request failed: %s', 'liens-morts-detector-jlg'),
565+
$exception->getMessage()
566+
)
567+
);
568+
}
569+
570+
fclose($handle);
571+
572+
$response = [
573+
'body' => $requestsResponse->body,
574+
'headers' => $requestsResponse->headers instanceof \Requests_Response_Headers
575+
? $requestsResponse->headers->getAll()
576+
: [],
577+
'response' => [
578+
'code' => $requestsResponse->status_code,
579+
'message' => $requestsResponse->status_text,
580+
],
581+
];
582+
} else {
583+
if (!function_exists('wp_remote_request')) {
584+
return new \WP_Error(
585+
'blc_s3_http_unavailable',
586+
__('The WordPress HTTP API is unavailable for S3 requests.', 'liens-morts-detector-jlg')
587+
);
588+
}
589+
590+
$handle = fopen($file_path, 'rb');
591+
if ($handle === false) {
592+
return new \WP_Error(
593+
'blc_s3_file_unreadable',
594+
__('Unable to open the generated CSV for S3 export.', 'liens-morts-detector-jlg')
595+
);
596+
}
597+
598+
$body = stream_get_contents($handle);
599+
fclose($handle);
600+
601+
if ($body === false) {
602+
return new \WP_Error(
603+
'blc_s3_file_unreadable',
604+
__('Unable to open the generated CSV for S3 export.', 'liens-morts-detector-jlg')
605+
);
606+
}
607+
608+
$response = wp_remote_request($url, [
609+
'method' => 'PUT',
610+
'headers' => $headers,
611+
'body' => $body,
612+
'timeout' => $timeout,
613+
]);
614+
}
537615

538616
if (blc_is_wp_error($response)) {
539617
return $response;

tests/Scanner/RemoteRequestClientTest.php

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,4 +309,52 @@ public function test_request_rotates_proxy_after_failure(): void
309309
$this->assertArrayHasKey('proxy-a', $health);
310310
$this->assertGreaterThan(0, $health['proxy-a']['failure_count']);
311311
}
312+
313+
public function test_get_request_falls_back_to_wp_remote_when_host_blocked(): void
314+
{
315+
$captured = [];
316+
317+
Functions\when('wp_safe_remote_get')->alias(static function () {
318+
return new \WP_Error('http_request_not_allowed', 'Host blocked by WP HTTP API');
319+
});
320+
321+
Functions\when('wp_remote_get')->alias(function ($url, $args) use (&$captured) {
322+
$captured[] = [$url, $args];
323+
324+
return ['response' => ['code' => 200]];
325+
});
326+
327+
$client = new RemoteRequestClient();
328+
329+
$response = $client->get('https://blocked.example', ['timeout' => 8]);
330+
331+
$this->assertSame(['response' => ['code' => 200]], $response);
332+
$this->assertCount(1, $captured);
333+
$this->assertSame('https://blocked.example', $captured[0][0]);
334+
$this->assertSame(8, $captured[0][1]['timeout']);
335+
}
336+
337+
public function test_head_request_falls_back_to_wp_remote_when_host_blocked(): void
338+
{
339+
$captured = [];
340+
341+
Functions\when('wp_safe_remote_head')->alias(static function () {
342+
return new \WP_Error('http_request_not_allowed', 'Host blocked by WP HTTP API');
343+
});
344+
345+
Functions\when('wp_remote_head')->alias(function ($url, $args) use (&$captured) {
346+
$captured[] = [$url, $args];
347+
348+
return ['response' => ['code' => 200]];
349+
});
350+
351+
$client = new RemoteRequestClient();
352+
353+
$response = $client->head('https://blocked.example', ['timeout' => 5]);
354+
355+
$this->assertSame(['response' => ['code' => 200]], $response);
356+
$this->assertCount(1, $captured);
357+
$this->assertSame('https://blocked.example', $captured[0][0]);
358+
$this->assertSame(5, $captured[0][1]['timeout']);
359+
}
312360
}

0 commit comments

Comments
 (0)