Skip to content

Commit b33c70d

Browse files
authored
Make use of Guzzle Pool to improve efficiency (#401)
* Make use of Guzzle Pool to improve efficiency * Fix documentation: add concurrency parameter where needed * fix checks: add a comma for php-cs-fixer * Add tests for flushPooled * refactor: rename concurrency parameter to requestConcurrency * readme: mention pooling for scaling
1 parent 2245c8d commit b33c70d

File tree

3 files changed

+105
-9
lines changed

3 files changed

+105
-9
lines changed

README.md

+7-6
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ $notifications = [
6060
'payload' => '{"message":"Hello World!"}',
6161
], [
6262
// current PushSubscription format (browsers might change this in the future)
63-
'subscription' => Subscription::create([
63+
'subscription' => Subscription::create([
6464
"endpoint" => "https://example.com/other/endpoint/of/another/vendor/abcdef...",
6565
"keys" => [
6666
'p256dh' => '(stringOf88Chars)',
@@ -253,18 +253,18 @@ foreach ($webPush->flush() as $report) {
253253
echo "[v] Message sent successfully for subscription {$endpoint}.";
254254
} else {
255255
echo "[x] Message failed to sent for subscription {$endpoint}: {$report->getReason()}";
256-
256+
257257
// also available (to get more info)
258-
258+
259259
/** @var \Psr\Http\Message\RequestInterface $requestToPushService */
260260
$requestToPushService = $report->getRequest();
261-
261+
262262
/** @var \Psr\Http\Message\ResponseInterface $responseOfPushService */
263263
$responseOfPushService = $report->getResponse();
264-
264+
265265
/** @var string $failReason */
266266
$failReason = $report->getReason();
267-
267+
268268
/** @var bool $isTheEndpointWrongOrExpired */
269269
$isTheEndpointWrongOrExpired = $report->isSubscriptionExpired();
270270
}
@@ -364,6 +364,7 @@ Here are some ideas:
364364
1. Make sure MultiCurl is available on your server
365365
2. Find the right balance for your needs between security and performance (see above)
366366
3. Find the right batch size (set it in `defaultOptions` or as parameter to `flush()`)
367+
4. Use `flushPooled()` instead of `flush()`. The former uses concurrent requests, accelerating the process and often doubling the speed of the requests.
367368

368369
### How to solve "SSL certificate problem: unable to get local issuer certificate"?
369370

src/WebPush.php

+58-3
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
namespace Minishlink\WebPush;
1515

1616
use GuzzleHttp\Client;
17+
use GuzzleHttp\Pool;
1718
use GuzzleHttp\Exception\RequestException;
1819
use GuzzleHttp\Psr7\Request;
1920
use ParagonIE\ConstantTime\Base64UrlSafe;
@@ -30,7 +31,7 @@ class WebPush
3031
protected ?array $notifications = null;
3132

3233
/**
33-
* @var array Default options: TTL, urgency, topic, batchSize
34+
* @var array Default options: TTL, urgency, topic, batchSize, requestConcurrency
3435
*/
3536
protected array $defaultOptions;
3637

@@ -53,7 +54,7 @@ class WebPush
5354
* WebPush constructor.
5455
*
5556
* @param array $auth Some servers need authentication
56-
* @param array $defaultOptions TTL, urgency, topic, batchSize
57+
* @param array $defaultOptions TTL, urgency, topic, batchSize, requestConcurrency
5758
* @param int|null $timeout Timeout of POST request
5859
*
5960
* @throws \ErrorException
@@ -175,6 +176,58 @@ public function flush(?int $batchSize = null): \Generator
175176
}
176177
}
177178

179+
/**
180+
* Flush notifications. Triggers concurrent requests.
181+
*
182+
* @param callable(MessageSentReport): void $callback Callback for each notification
183+
* @param null|int $batchSize Defaults the value defined in defaultOptions during instantiation (which defaults to 1000).
184+
* @param null|int $requestConcurrency Defaults the value defined in defaultOptions during instantiation (which defaults to 100).
185+
*/
186+
public function flushPooled($callback, ?int $batchSize = null, ?int $requestConcurrency = null): void
187+
{
188+
if (empty($this->notifications)) {
189+
return;
190+
}
191+
192+
if (null === $batchSize) {
193+
$batchSize = $this->defaultOptions['batchSize'];
194+
}
195+
196+
if (null === $requestConcurrency) {
197+
$requestConcurrency = $this->defaultOptions['requestConcurrency'];
198+
}
199+
200+
$batches = array_chunk($this->notifications, $batchSize);
201+
$this->notifications = [];
202+
203+
foreach ($batches as $batch) {
204+
$batch = $this->prepare($batch);
205+
$pool = new Pool($this->client, $batch, [
206+
'requestConcurrency' => $requestConcurrency,
207+
'fulfilled' => function (ResponseInterface $response, int $index) use ($callback, $batch) {
208+
/** @var \Psr\Http\Message\RequestInterface $request **/
209+
$request = $batch[$index];
210+
$callback(new MessageSentReport($request, $response));
211+
},
212+
'rejected' => function (RequestException $reason) use ($callback) {
213+
if (method_exists($reason, 'getResponse')) {
214+
$response = $reason->getResponse();
215+
} else {
216+
$response = null;
217+
}
218+
$callback(new MessageSentReport($reason->getRequest(), $response, false, $reason->getMessage()));
219+
},
220+
]);
221+
222+
$promise = $pool->promise();
223+
$promise->wait();
224+
}
225+
226+
if ($this->reuseVAPIDHeaders) {
227+
$this->vapidHeaders = [];
228+
}
229+
}
230+
178231
/**
179232
* @throws \ErrorException|\Random\RandomException
180233
*/
@@ -315,14 +368,16 @@ public function getDefaultOptions(): array
315368
}
316369

317370
/**
318-
* @param array $defaultOptions Keys 'TTL' (Time To Live, defaults 4 weeks), 'urgency', 'topic', 'batchSize'
371+
* @param array $defaultOptions Keys 'TTL' (Time To Live, defaults 4 weeks), 'urgency', 'topic', 'batchSize', 'requestConcurrency'
319372
*/
320373
public function setDefaultOptions(array $defaultOptions): WebPush
321374
{
322375
$this->defaultOptions['TTL'] = $defaultOptions['TTL'] ?? 2419200;
323376
$this->defaultOptions['urgency'] = $defaultOptions['urgency'] ?? null;
324377
$this->defaultOptions['topic'] = $defaultOptions['topic'] ?? null;
325378
$this->defaultOptions['batchSize'] = $defaultOptions['batchSize'] ?? 1000;
379+
$this->defaultOptions['requestConcurrency'] = $defaultOptions['requestConcurrency'] ?? 100;
380+
326381

327382
return $this;
328383
}

tests/WebPushTest.php

+40
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,46 @@ public function testFlush(): void
222222
}
223223
}
224224

225+
/**
226+
* @throws \ErrorException
227+
* @throws \JsonException
228+
*/
229+
public function testFlushPooled(): void
230+
{
231+
$subscription = new Subscription(self::$endpoints['standard']);
232+
233+
$report = $this->webPush->sendOneNotification($subscription);
234+
$this->assertFalse($report->isSuccess()); // it doesn't have VAPID
235+
236+
// queue has been reset
237+
$this->assertEmpty(iterator_to_array($this->webPush->flush()));
238+
239+
$report = $this->webPush->sendOneNotification($subscription);
240+
$this->assertFalse($report->isSuccess()); // it doesn't have VAPID
241+
242+
$nonExistentSubscription = Subscription::create([
243+
'endpoint' => 'https://fcm.googleapis.com/fcm/send/fCd2-8nXJhU:APA91bGi2uaqFXGft4qdolwyRUcUPCL1XV_jWy1tpCRqnu4sk7ojUpC5gnq1PTncbCdMq9RCVQIIFIU9BjzScvjrDqpsI7J-K_3xYW8xo1xSNCfge1RvJ6Xs8RGL_Sw7JtbCyG1_EVgWDc22on1r_jozD8vsFbB0Fg',
244+
'publicKey' => 'BME-1ZSAv2AyGjENQTzrXDj6vSnhAIdKso4n3NDY0lsd1DUgEzBw7ARMKjrYAm7JmJBPsilV5CWNH0mVPyJEt0Q',
245+
'authToken' => 'hUIGbmiypj9_EQea8AnCKA',
246+
'contentEncoding' => 'aes128gcm',
247+
]);
248+
249+
// test multiple requests
250+
$this->webPush->queueNotification($nonExistentSubscription, json_encode(['test' => 1], JSON_THROW_ON_ERROR));
251+
$this->webPush->queueNotification($nonExistentSubscription, json_encode(['test' => 2], JSON_THROW_ON_ERROR));
252+
$this->webPush->queueNotification($nonExistentSubscription, json_encode(['test' => 3], JSON_THROW_ON_ERROR));
253+
254+
$callback = function ($report) {
255+
$this->assertFalse($report->isSuccess());
256+
$this->assertTrue($report->isSubscriptionExpired());
257+
$this->assertEquals(410, $report->getResponse()->getStatusCode());
258+
$this->assertNotEmpty($report->getReason());
259+
$this->assertNotFalse(filter_var($report->getEndpoint(), FILTER_VALIDATE_URL));
260+
};
261+
262+
$this->webPush->flushPooled($callback);
263+
}
264+
225265
public function testFlushEmpty(): void
226266
{
227267
$this->assertEmpty(iterator_to_array($this->webPush->flush(300)));

0 commit comments

Comments
 (0)