Skip to content

Commit

Permalink
Merge pull request #28 from justbetter/feature/availability
Browse files Browse the repository at this point in the history
Add availability
  • Loading branch information
VincentBean authored Nov 12, 2024
2 parents a23c6bf + 5516afd commit b7d8c3c
Show file tree
Hide file tree
Showing 20 changed files with 398 additions and 6 deletions.
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

0 comments on commit b7d8c3c

Please sign in to comment.