Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add availability #28

Merged
merged 2 commits into from
Nov 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,10 @@ Http::fake([
$item = Item::query()->first();
```

## Availability

This client can prevent requests from going to Dynamics when it is giving HTTP status codes 503, 504 or timeouts. This can be configured per connection in the `availability` settings. Enable the `throw` option to prevent any requests from going to Dynamics.

## Contributing

Please see [CONTRIBUTING](.github/CONTRIBUTING.md) for details.
Expand Down
16 changes: 16 additions & 0 deletions config/dynamics.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,22 @@
'options' => [
'connect_timeout' => 5,
],
'availability' => [
/* The response codes that should trigger the availability check in addition to connection timeouts */
'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,

/* Throw an exception that prevents calls to Dynamics when unavailable */
'throw' => false,
],
],
],

Expand Down
20 changes: 20 additions & 0 deletions src/Actions/Availability/CheckAvailability.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

namespace JustBetter\DynamicsClient\Actions\Availability;

use JustBetter\DynamicsClient\Contracts\Availability\ChecksAvailability;

class CheckAvailability implements ChecksAvailability
{
const AVAILABLE_KEY = 'dynamics-client:availability:';

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

public static function bind(): void
{
app()->singleton(ChecksAvailability::class, static::class);
}
}
41 changes: 41 additions & 0 deletions src/Actions/Availability/RegisterUnavailability.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

namespace JustBetter\DynamicsClient\Actions\Availability;

use JustBetter\DynamicsClient\Contracts\Availability\RegistersUnavailability;

class RegisterUnavailability implements RegistersUnavailability
{
public const COUNT_KEY = 'dynamics-client:unavailable-count:';

public function register(string $connection): void
{
$countKey = static::COUNT_KEY.$connection;

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

/** @var int $threshold */
$threshold = config('dynamics.connections.'.$connection.'.availability.threshold', 10);

/** @var int $timespan */
$timespan = config('dynamics.connections.'.$connection.'.availability.timespan', 10);

/** @var int $cooldown */
$cooldown = config('dynamics.connections.'.$connection.'.availability.cooldown', 2);

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

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

cache()->forget($countKey);
}
}

public static function bind(): void
{
app()->singleton(RegistersUnavailability::class, static::class);
}
}
7 changes: 6 additions & 1 deletion src/Client/ClientFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ public function oauth(string $connection, array $config): static

public function fabricate(): ODataClient
{
$httpProvider = new ClientHttpProvider;
$httpProvider = new ClientHttpProvider($this->connection);
$httpProvider->setExtraOptions($this->options);

return new ODataClient($this->url, null, $httpProvider);
Expand All @@ -131,4 +131,9 @@ public function when(bool $condition, Closure $callback): ClientFactory

return $this;
}

public static function bind(): void
{
app()->bind(ClientFactoryContract::class, static::class);
}
}
30 changes: 30 additions & 0 deletions src/Client/ClientHttpProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,35 @@

namespace JustBetter\DynamicsClient\Client;

use Illuminate\Http\Client\ConnectionException;
use Illuminate\Http\Client\RequestException;
use Illuminate\Support\Facades\Http;
use JustBetter\DynamicsClient\Contracts\Availability\ChecksAvailability;
use JustBetter\DynamicsClient\Events\DynamicsResponseEvent;
use JustBetter\DynamicsClient\Events\DynamicsTimeoutEvent;
use JustBetter\DynamicsClient\Exceptions\DynamicsException;
use JustBetter\DynamicsClient\Exceptions\ModifiedException;
use JustBetter\DynamicsClient\Exceptions\NotFoundException;
use JustBetter\DynamicsClient\Exceptions\UnavailableException;
use Psr\Http\Message\ResponseInterface;
use SaintSystems\OData\GuzzleHttpProvider;
use SaintSystems\OData\HttpRequestMessage;

class ClientHttpProvider extends GuzzleHttpProvider
{
public function __construct(protected string $connection)
{
parent::__construct();
}

public function send(HttpRequestMessage $request): ResponseInterface
{
$throw = config('dynamics.connections.'.$this->connection.'.availability.throw', false);

if ($throw && ! $this->available()) {
throw new UnavailableException('The Dynamics connection "'.$this->connection.'" is currently unavailable.');
}

try {
$options = $this->extra_options;

Expand All @@ -23,6 +39,8 @@ public function send(HttpRequestMessage $request): ResponseInterface
}

$response = Http::send($request->method, $request->requestUri, $options);
DynamicsResponseEvent::dispatch($response, $this->connection);

$response->throw();

return $response->toPsrResponse();
Expand All @@ -42,6 +60,18 @@ public function send(HttpRequestMessage $request): ResponseInterface
throw $dynamicsException
->setRequest($request)
->setResponse($exception->response);
} catch (ConnectionException $e) {
DynamicsTimeoutEvent::dispatch($this->connection);

throw $e;
}
}

protected function available(): bool
{
/** @var ChecksAvailability $checker */
$checker = app(ChecksAvailability::class);

return $checker->check($this->connection);
}
}
8 changes: 8 additions & 0 deletions src/Contracts/Availability/ChecksAvailability.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

namespace JustBetter\DynamicsClient\Contracts\Availability;

interface ChecksAvailability
{
public function check(string $connection): bool;
}
8 changes: 8 additions & 0 deletions src/Contracts/Availability/RegistersUnavailability.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

namespace JustBetter\DynamicsClient\Contracts\Availability;

interface RegistersUnavailability
{
public function register(string $connection): void;
}
16 changes: 16 additions & 0 deletions src/Events/DynamicsResponseEvent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

namespace JustBetter\DynamicsClient\Events;

use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Http\Client\Response;

class DynamicsResponseEvent
{
use Dispatchable;

public function __construct(
public Response $response,
public string $connection,
) {}
}
14 changes: 14 additions & 0 deletions src/Events/DynamicsTimeoutEvent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

namespace JustBetter\DynamicsClient\Events;

use Illuminate\Foundation\Events\Dispatchable;

class DynamicsTimeoutEvent
{
use Dispatchable;

public function __construct(
public string $connection,
) {}
}
5 changes: 5 additions & 0 deletions src/Exceptions/UnavailableException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?php

namespace JustBetter\DynamicsClient\Exceptions;

class UnavailableException extends DynamicsException {}
23 changes: 23 additions & 0 deletions src/Listeners/ResponseAvailabilityListener.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

namespace JustBetter\DynamicsClient\Listeners;

use JustBetter\DynamicsClient\Contracts\Availability\RegistersUnavailability;
use JustBetter\DynamicsClient\Events\DynamicsResponseEvent;

class ResponseAvailabilityListener
{
public function __construct(protected RegistersUnavailability $unavailability) {}

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

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

$this->unavailability->register($event->connection);
}
}
16 changes: 16 additions & 0 deletions src/Listeners/TimeoutAvailabilityListener.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

namespace JustBetter\DynamicsClient\Listeners;

use JustBetter\DynamicsClient\Contracts\Availability\RegistersUnavailability;
use JustBetter\DynamicsClient\Events\DynamicsTimeoutEvent;

class TimeoutAvailabilityListener
{
public function __construct(protected RegistersUnavailability $unavailability) {}

public function handle(DynamicsTimeoutEvent $event): void
{
$this->unavailability->register($event->connection);
}
}
9 changes: 9 additions & 0 deletions src/OData/BaseResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use JustBetter\DynamicsClient\Concerns\HasCasts;
use JustBetter\DynamicsClient\Concerns\HasData;
use JustBetter\DynamicsClient\Concerns\HasKeys;
use JustBetter\DynamicsClient\Contracts\Availability\ChecksAvailability;
use JustBetter\DynamicsClient\Contracts\ClientFactoryContract;
use JustBetter\DynamicsClient\Exceptions\DynamicsException;
use JustBetter\DynamicsClient\Query\QueryBuilder;
Expand Down Expand Up @@ -159,6 +160,14 @@ public function relation(string $relation, string $class): QueryBuilder
return new QueryBuilder($this->client(), $this->connection, $this->getResourceUrl().'/'.$relation, $class);
}

public function available(): bool
{
/** @var ChecksAvailability $instance */
$instance = app(ChecksAvailability::class);

return $instance->check($this->connection);
}

public static function fake(): void
{
foreach (config('dynamics.connections') as $connection => $data) {
Expand Down
27 changes: 22 additions & 5 deletions src/ServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,24 @@

namespace JustBetter\DynamicsClient;

use Illuminate\Support\Facades\Event;
use Illuminate\Support\ServiceProvider as BaseServiceProvider;
use JustBetter\DynamicsClient\Actions\Availability\CheckAvailability;
use JustBetter\DynamicsClient\Actions\Availability\RegisterUnavailability;
use JustBetter\DynamicsClient\Client\ClientFactory;
use JustBetter\DynamicsClient\Commands\TestConnection;
use JustBetter\DynamicsClient\Contracts\ClientFactoryContract;
use JustBetter\DynamicsClient\Events\DynamicsResponseEvent;
use JustBetter\DynamicsClient\Events\DynamicsTimeoutEvent;
use JustBetter\DynamicsClient\Listeners\ResponseAvailabilityListener;
use JustBetter\DynamicsClient\Listeners\TimeoutAvailabilityListener;

class ServiceProvider extends BaseServiceProvider
{
public function register(): void
{
$this
->registerConfig()
->bindResolvers();
->registerActions();
}

protected function registerConfig(): static
Expand All @@ -23,9 +29,11 @@ protected function registerConfig(): static
return $this;
}

protected function bindResolvers(): static
protected function registerActions(): static
{
$this->app->bind(ClientFactoryContract::class, ClientFactory::class);
ClientFactory::bind();
CheckAvailability::bind();
RegisterUnavailability::bind();

return $this;
}
Expand All @@ -34,7 +42,8 @@ public function boot(): void
{
$this
->bootConfig()
->bootCommands();
->bootCommands()
->bootListeners();
}

protected function bootConfig(): static
Expand All @@ -56,4 +65,12 @@ protected function bootCommands(): static

return $this;
}

protected function bootListeners(): static
{
Event::listen(DynamicsTimeoutEvent::class, TimeoutAvailabilityListener::class);
Event::listen(DynamicsResponseEvent::class, ResponseAvailabilityListener::class);

return $this;
}
}
23 changes: 23 additions & 0 deletions tests/Actions/Availability/CheckAvailabilityTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

namespace JustBetter\DynamicsClient\Tests\Actions\Availability;

use JustBetter\DynamicsClient\Actions\Availability\CheckAvailability;
use JustBetter\DynamicsClient\Tests\TestCase;
use PHPUnit\Framework\Attributes\Test;

class CheckAvailabilityTest extends TestCase
{
#[Test]
public function it_checks_availability(): void
{
/** @var CheckAvailability $action */
$action = app(CheckAvailability::class);

$this->assertTrue($action->check('default'));

cache()->put(CheckAvailability::AVAILABLE_KEY.'default', false);

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