Skip to content

Commit

Permalink
Added an availability method to check if Magento is reachable (#42)
Browse files Browse the repository at this point in the history
Added an availability method to check if Magento is reachable
  • Loading branch information
VincentBean authored Aug 23, 2024
1 parent 64164a1 commit 178cc9a
Show file tree
Hide file tree
Showing 16 changed files with 379 additions and 6 deletions.
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,18 @@ $client->graphql(

You can view the tests for more examples.

### Checking if Magento is available

This client keeps track whether Magento is available by storing the count of any >=502 and <=504 response code in cache.
You may use the `available()` method on the client in order to check if Magento is available.

The threshold, timespan and status codes can be configured in the configuration file per connection.

#### Jobs

This package provides a job middleware that releases jobs back onto the queue when Magento is not available.
The middleware is located at `\JustBetter\MagentoClient\Jobs\Middleware\AvailableMiddleware`.

## Testing

This package uses Laravel's HTTP client so that you can fake the requests.
Expand Down
15 changes: 15 additions & 0 deletions config/magento.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,21 @@

/* Authentication method, choose either "oauth" or "token". */
'authentication_method' => env('MAGENTO_AUTH_METHOD', 'token'),

/* Availability configuration. */
'availability' => [
/* The response codes that should trigger the availability check. */
'codes' => [502, 503, 504],

/* The amount of failed requests before the service is marked as unavailable. */
'threshold' => 10,

/* The timespan in minutes in which the failed requests should occur. */
'timespan' => 10,

/* The cooldown in minutes after the threshold is reached. */
'cooldown' => 2,
],
],
],

Expand Down
3 changes: 2 additions & 1 deletion phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ parameters:
- src
- tests
level: 8
checkMissingIterableValueType: false
ignoreErrors:
- identifier: missingType.iterableValue
20 changes: 20 additions & 0 deletions src/Actions/CheckMagento.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

namespace JustBetter\MagentoClient\Actions;

use JustBetter\MagentoClient\Contracts\ChecksMagento;

class CheckMagento implements ChecksMagento
{
public const AVAILABLE_KEY = 'magento-client:available:';

public function available(string $connection): bool
{
return cache()->get(static::AVAILABLE_KEY.$connection, true);
}

public static function bind(): void
{
app()->singleton(ChecksMagento::class, static::class);
}
}
11 changes: 9 additions & 2 deletions src/Client/Magento.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use Illuminate\Support\Enumerable;
use Illuminate\Support\LazyCollection;
use JustBetter\MagentoClient\Contracts\BuildsRequest;
use JustBetter\MagentoClient\Contracts\ChecksMagento;
use JustBetter\MagentoClient\Events\MagentoResponseEvent;
use JustBetter\MagentoClient\OAuth\KeyStore\FileKeyStore;

Expand All @@ -21,7 +22,8 @@ class Magento
public ?Closure $interceptor;

public function __construct(
protected BuildsRequest $request
protected BuildsRequest $request,
protected ChecksMagento $checksMagento,
) {
$this->connection = config('magento.connection');
}
Expand Down Expand Up @@ -185,6 +187,11 @@ public function lazy(string $path, array $data = [], int $pageSize = 100): LazyC
});
}

public function available(): bool
{
return $this->checksMagento->available($this->connection);
}

public function getUrl(string $path, bool $async = false, bool $bulk = false): string
{
/** @var array $config */
Expand Down Expand Up @@ -230,7 +237,7 @@ protected function request(): PendingRequest

protected function handleResponse(Response $response): Response
{
MagentoResponseEvent::dispatch($response);
MagentoResponseEvent::dispatch($response, $this->connection);

return $response;
}
Expand Down
8 changes: 8 additions & 0 deletions src/Contracts/ChecksMagento.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

namespace JustBetter\MagentoClient\Contracts;

interface ChecksMagento
{
public function available(string $connection): bool;
}
4 changes: 2 additions & 2 deletions src/Events/MagentoResponseEvent.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ class MagentoResponseEvent
use Dispatchable;

public function __construct(
public Response $response
public Response $response,
public string $connection,
) {

}
}
32 changes: 32 additions & 0 deletions src/Jobs/Middleware/AvailableMiddleware.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

namespace JustBetter\MagentoClient\Jobs\Middleware;

use Closure;
use JustBetter\MagentoClient\Client\Magento;

class AvailableMiddleware
{
protected string $connection;

protected int $seconds;

public function __construct(?string $connection = null, int $seconds = 5)
{
$this->connection = $connection ?? config('magento.connection');
$this->seconds = $seconds;
}

public function handle(object $job, Closure $next): void
{
/** @var Magento $magento */
$magento = app(Magento::class);
$magento->connection($this->connection);

if ($magento->available()) {
$next($job);
} elseif (method_exists($job, 'release')) {
$job->release($this->seconds);
}
}
}
44 changes: 44 additions & 0 deletions src/Listeners/StoreAvailabilityListener.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

namespace JustBetter\MagentoClient\Listeners;

use JustBetter\MagentoClient\Actions\CheckMagento;
use JustBetter\MagentoClient\Events\MagentoResponseEvent;

class StoreAvailabilityListener
{
public const COUNT_KEY = 'magento-client:response:count:unavailable:';

public function handle(MagentoResponseEvent $event): void
{
/** @var array<int, int> $codes */
$codes = config('magento.connections.'.$event->connection.'.availability.codes', [502, 503, 504]);

if (! in_array($event->response->status(), $codes)) {
return;
}

$countKey = static::COUNT_KEY.$event->connection;

/** @var int $count */
$count = cache()->get($countKey, 0);
$count++;

/** @var int $threshold */
$threshold = config('magento.connections.'.$event->connection.'.availability.threshold', 10);

/** @var int $timespan */
$timespan = config('magento.connections.'.$event->connection.'.availability.timespan', 10);

/** @var int $cooldown */
$cooldown = config('magento.connections.'.$event->connection.'.availability.cooldown', 2);

cache()->put($countKey, $count, now()->addMinutes($timespan));

if ($count >= $threshold) {
cache()->put(CheckMagento::AVAILABLE_KEY.$event->connection, false, now()->addMinutes($cooldown));

cache()->forget($countKey);
}
}
}
15 changes: 14 additions & 1 deletion src/ServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,16 @@

namespace JustBetter\MagentoClient;

use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\ServiceProvider as BaseServiceProvider;
use JustBetter\MagentoClient\Actions\AuthenticateRequest;
use JustBetter\MagentoClient\Actions\BuildRequest;
use JustBetter\MagentoClient\Actions\CheckMagento;
use JustBetter\MagentoClient\Actions\OAuth\RequestAccessToken;
use JustBetter\MagentoClient\Events\MagentoResponseEvent;
use JustBetter\MagentoClient\Http\Middleware\OAuthMiddleware;
use JustBetter\MagentoClient\Listeners\StoreAvailabilityListener;

class ServiceProvider extends BaseServiceProvider
{
Expand All @@ -30,6 +34,7 @@ protected function registerActions(): static
RequestAccessToken::bind();
AuthenticateRequest::bind();
BuildRequest::bind();
CheckMagento::bind();

return $this;
}
Expand All @@ -38,6 +43,7 @@ public function boot(): void
{
$this
->bootConfig()
->bootEvents()
->bootRoutes()
->bootMigrations();
}
Expand All @@ -51,9 +57,16 @@ protected function bootConfig(): static
return $this;
}

protected function bootEvents(): static
{
Event::listen(MagentoResponseEvent::class, StoreAvailabilityListener::class);

return $this;
}

protected function bootRoutes(): static
{
if (! $this->app->routesAreCached()) {
if (! app()->routesAreCached()) {
Route::prefix(config('magento.oauth.prefix'))
->middleware([OAuthMiddleware::class])
->group(__DIR__.'/../routes/web.php');
Expand Down
43 changes: 43 additions & 0 deletions tests/Actions/CheckMagentoTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

namespace JustBetter\MagentoClient\Tests\Actions;

use JustBetter\MagentoClient\Actions\CheckMagento;
use JustBetter\MagentoClient\Tests\TestCase;
use PHPUnit\Framework\Attributes\Test;

class CheckMagentoTest extends TestCase
{
#[Test]
public function it_can_be_available(): void
{
/** @var CheckMagento $action */
$action = app(CheckMagento::class);

$this->assertTrue($action->available('default'));
}

#[Test]
public function it_can_be_unavailable(): void
{
/** @var CheckMagento $action */
$action = app(CheckMagento::class);

cache()->put(CheckMagento::AVAILABLE_KEY.config('magento.connection'), false);

$this->assertFalse($action->available('default'));
}

#[Test]
public function it_can_handle_multiple_connections(): void
{
/** @var CheckMagento $action */
$action = app(CheckMagento::class);

cache()->put(CheckMagento::AVAILABLE_KEY.'connection', false);
cache()->put(CheckMagento::AVAILABLE_KEY.'another-connection', true);

$this->assertFalse($action->available('connection'));
$this->assertTrue($action->available('another-connection'));
}
}
16 changes: 16 additions & 0 deletions tests/Client/ClientTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;
use JustBetter\MagentoClient\Client\Magento;
use JustBetter\MagentoClient\Contracts\ChecksMagento;
use JustBetter\MagentoClient\Events\MagentoResponseEvent;
use JustBetter\MagentoClient\Tests\TestCase;
use Mockery\MockInterface;

class ClientTest extends TestCase
{
Expand Down Expand Up @@ -592,4 +594,18 @@ public function test_it_dispatches_event(): void
return $event->response->ok() && $event->response->json('items') === ['item'];
});
}

public function test_it_checks_available(): void
{
$this->mock(ChecksMagento::class, function (MockInterface $mock): void {
$mock->shouldReceive('available')->with('default')->once()->andReturnTrue();
$mock->shouldReceive('available')->with('unavailable')->once()->andReturnFalse();
});

/** @var Magento $magento */
$magento = app(Magento::class);

$this->assertTrue($magento->available());
$this->assertFalse($magento->connection('unavailable')->available());
}
}
35 changes: 35 additions & 0 deletions tests/Enums/AuthenticationMethodTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

namespace JustBetter\MagentoClient\Tests\Enums;

use JustBetter\MagentoClient\Enums\AuthenticationMethod;
use JustBetter\MagentoClient\Providers\BearerTokenProvider;
use JustBetter\MagentoClient\Providers\OAuthProvider;
use JustBetter\MagentoClient\Tests\TestCase;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;

class AuthenticationMethodTest extends TestCase
{
#[Test]
#[DataProvider('providers')]
public function it_can_get_the_provider(AuthenticationMethod $authenticationMethod, string $expectedProvider): void
{
/** @var class-string $expectedProvider */
$this->assertInstanceOf($expectedProvider, $authenticationMethod->provider());
}

public static function providers(): array
{
return [
[
AuthenticationMethod::Token,
BearerTokenProvider::class,
],
[
AuthenticationMethod::OAuth,
OAuthProvider::class,
],
];
}
}
21 changes: 21 additions & 0 deletions tests/Fakes/TestJob.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

namespace JustBetter\MagentoClient\Tests\Fakes;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;

class TestJob implements ShouldBeUnique, ShouldQueue
{
use Dispatchable;
use InteractsWithQueue;
use Queueable;

public function handle(): void
{
//
}
}
Loading

0 comments on commit 178cc9a

Please sign in to comment.