GithubHelp home page GithubHelp logo

ackintosh / ganesha Goto Github PK

View Code? Open in Web Editor NEW
554.0 22.0 40.0 801 KB

:elephant: A Circuit Breaker pattern implementation for PHP applications.

Home Page: https://ackintosh.github.io/ganesha/

License: MIT License

PHP 100.00%
circuit-breaker php fault-tolerance circuit-breaker-pattern guzzle guzzle-middleware microservice microservices

ganesha's People

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

ganesha's Issues

Symfony HTTPClient ServiceNameExtractor doesn't support base_uri

When extracting the service name from the URL, the ServiceNameExtractor doesn't use the base_uri option that is provided by the HTTPClient, and used with ScopedClients.

An InvalidArgumentException is thrown with the message Malformed URL "[url]".

Ackintosh\Ganesha\HttpClient\HostTrait:12

Rename TimeWindow

Rename to appropriate name.

  • FixedTimeWindow -> TumblingTimeWindow
  • RollingTimeWindow -> SlidingTimeWindow

Redis Adapter does not initialise status

The Redis adapter will never be able to loadStatus successfully as it will always exit with Failed to load status. The status is therefor never set. In the same situation in the Memcached adapter it will initialise the status with Ganesha::STATUS_CALMED_DOWN.

The issues can clearly be seen by subscribing to the Ganesha instance.

MongoDB adapter - SlidingTimeWindow support

Thanks for this great package!

I'm a bit confused about MongoDB implementation. It is stated in the README that an adapter can support either SlidingTimeWindow or TumblingTimeWindow, however this is not the case for MongoDB adapter as far as I can tell from the implemented interfaces.

I believe MongoDB adapter does not support SlidingTimeWindow the same way Redis Adapter does. It rather resets the stats every whole time window (more like TumblingTimeWindow but without comparing to the previous window). I came to this conclusion after checking the adapter data model and the Rate strategy and I was wondering if that is by design or a bug?

Success key cleanup - Redis adapter

So, first of all, thank you so much for the library. We use it in multiple projects and it works like a charm!

But recently, we had some troubles regarding Redis success keys. Since the success count doesn't get cleaned up, if the circuit breaker doesn't trip, it gets to a point where our Redis instance fills up and it needs to get flushed manually.

I think something similar was reported some time ago, but seems like it didn't get anywhere.

To get by, I implemented custom Redis and RedisStore adapters, so I can set a TTL to the key. We are currently testing it and seems like there are no hiccups.

I don't want to make a PR with those changes since I think it's not the best approach, so I would like to know your oppinion about what might be the correct solution to this.

Redis example

Currently example runs with memcached. At the same time, I recommend Redis adapter because it has SlidingWindow which is appropriate for failure rate calculation. So I want provide Redis example.

Redis Success key keeps growing

When using Ganesha with the Rate + Redis Adapter i noticed that the redis key for successes keeps growing and growing until a failure occurs.
This seems to be because storage->getSuccessCount() which calls zRemRangeByScore as part of load() is never called meaning that the key is never cleaned out.

Maybe adding a cleanup for these keys based on some sort of sampling concept is a good idea?

Better validation for build parameters

The numbers passed to Builder ( e.g. timeWindow, failureRateThreshold and so on) is not validated.

Better validation for the parameters would be helpful to avoid unintensional behavior due to wrong configuration.

  • positive numbers?
  • within specified range?

Adapter Redis

GaneshaTest.php
`namespace Ackintosh;

use Ackintosh\Ganesha\Builder;
use Ackintosh\Ganesha\Storage\Adapter\Redis;

class GaneshaTest extends \PHPUnit_Framework_TestCase
{
/**
* @var string
*/
private $service = 'GaneshaTestService';

/**
 * @var \Redis
 */
private $r;

public function setUp()
{
    parent::setUp();
    $this->r = new \Redis();
    $this->r->connect('127.0.0.1');
    $this->r->flushAll();
}

/**
 * @test
 */
public function recordsFailureAndTrips()
{
    $ganesha = $this->buildGanesha(2);
    $this->assertTrue($ganesha->isAvailable($this->service));

    $ganesha->failure($this->service);
    $ganesha->failure($this->service);
    $this->assertFalse($ganesha->isAvailable($this->service));
    // it does not affect other services.
    $this->assertTrue($ganesha->isAvailable('other' . $this->service));
}

/**
 * @test
 */
public function recordsSuccessAndClose()
{
    $ganesha = $this->buildGanesha(2);
    $ganesha->failure($this->service);
    $ganesha->failure($this->service);
    $this->assertFalse($ganesha->isAvailable($this->service));

    $ganesha->success($this->service);
    $this->assertTrue($ganesha->isAvailable($this->service));
}

/**
 * @test
 */
public function notifyTripped()
{
    $ganesha = $this->buildGanesha(
        2,
        10
    );

    $receiver = $this->getMockBuilder('\stdClass')
        ->setMethods(['receive'])
        ->getMock();
    $receiver->expects($this->once())
        ->method('receive')
        ->with(Ganesha::EVENT_TRIPPED, $this->service, '');

    $ganesha->subscribe(function ($event, $service, $message) use ($receiver) {
        $receiver->receive($event, $service, $message);
    });

    $ganesha->failure($this->service);
    $ganesha->failure($this->service);
}


/**
 * @test
 */
public function withMemcached()
{
    $ganesha = Builder::buildWithCountStrategy([
        'failureCountThreshold' => 1,
        'adapter' => new Redis($this->r),
        'intervalToHalfOpen' => 10,
    ]);

    $this->assertTrue($ganesha->isAvailable($this->service));
    $ganesha->failure($this->service);
    $this->assertFalse($ganesha->isAvailable($this->service));
}

/**
 * @test
 */
public function failureCountMustNotBeNegative()
{
    $ganesha = $this->buildGanesha(1);

    $ganesha->success($this->service);
    $ganesha->success($this->service);
    $ganesha->success($this->service);
    $this->assertTrue($ganesha->isAvailable($this->service));

    $ganesha->failure($this->service);
    $this->assertFalse($ganesha->isAvailable($this->service));
}

/**
 * @test
 */
public function withIntervalToHalfOpen()
{
    $ganesha = $this->buildGanesha(
        1,
        1
    );

    $this->assertTrue($ganesha->isAvailable($this->service));
    // record a failure, ganesha has trip
    $ganesha->failure($this->service);
    $this->assertFalse($ganesha->isAvailable($this->service));
    // wait for the interval to half-open
    sleep(2);
    // half-open
    $this->assertTrue($ganesha->isAvailable($this->service));
    // after half-open, service is not available until the interval has elapsed
    $this->assertFalse($ganesha->isAvailable($this->service));
    // record a success, ganesha has close
    $ganesha->success($this->service);
    $this->assertTrue($ganesha->isAvailable($this->service));
}

/**
 * @test
 */
public function disable()
{
    $ganesha = $this->buildGanesha(1);

    $ganesha->failure($this->service);
    $this->assertFalse($ganesha->isAvailable($this->service));

    Ganesha::disable();
    $this->assertTrue($ganesha->isAvailable($this->service));

    Ganesha::enable();
    $this->assertFalse($ganesha->isAvailable($this->service));
}

/**
 * @test
 */
public function reset()
{
    $ganesha = $this->buildGanesha(1);

    $ganesha->failure($this->service);
    $this->assertFalse($ganesha->isAvailable($this->service));

    // For making sure that \Memcached::getAllKeys() (be called by Ganesha::reset()) takes ALL keys, we need to wait a moment...
    sleep(1);

    $ganesha->reset();
    $this->assertTrue($ganesha->isAvailable($this->service));
}

/**
 * @test
 */
public function withRateStrategy()
{
    $ganesha = Builder::build([
        'adapter' => new Redis($this->r),
        'timeWindow' => 3,
        'failureRateThreshold' => 50,
        'minimumRequests' => 1,
        'intervalToHalfOpen' => 10,
    ]);

    $this->assertTrue($ganesha->isAvailable('test'));

    $ganesha->failure('test');
    $ganesha->failure('test');
    $ganesha->failure('test');
    $ganesha->success('test');
    $ganesha->success('test');

    $this->assertFalse($ganesha->isAvailable('test'));
}

private function buildGanesha(
    $threshold,
    $intervalToHalfOpen = 10
)
{
    return Builder::buildWithCountStrategy([
        'failureCountThreshold' => $threshold,
        'adapter' => new Redis($this->r),
        'intervalToHalfOpen' => $intervalToHalfOpen,
    ]);
}

}`

Test Result:
Failed asserting that true is false.
D:\wwwroot\ganesha\tests\Ackintosh\GaneshaTest.php:51

Failed asserting that true is false.
D:\wwwroot\ganesha\tests\Ackintosh\GaneshaTest.php:64

Expectation failed for method name is equal to string:receive when invoked 1 time(s).
Method was expected to be called 1 times, actually called 0 times.

Failed asserting that true is false.
D:\wwwroot\ganesha\tests\Ackintosh\GaneshaTest.php:109

Failed asserting that true is false.
D:\wwwroot\ganesha\tests\Ackintosh\GaneshaTest.php:125

Failed asserting that true is false.
D:\wwwroot\ganesha\tests\Ackintosh\GaneshaTest.php:141

Failed asserting that true is false.
D:\wwwroot\ganesha\tests\Ackintosh\GaneshaTest.php:161

Failed asserting that true is false.
D:\wwwroot\ganesha\tests\Ackintosh\GaneshaTest.php:178

Time: 863 ms, Memory: 8.00MB

FAILURES!
Tests: 9, Assertions: 14, Failures: 8.

Support for psr/http-message 2

Hi,

it would be nice to loosen the dependency on psr/http-message to something like ^1.1 || ^2.0 because quite a lot other packages depend on this package.

Thanks

1.2.0 Release backwards incompatibility

Hello and thanks for your great work on this package. Does this package follow the https://semver.org/ standard?

If it does not, we may have to look into an alternative as it is hard to commercially rely on a package which will make breaking changes in a minor release. The breaking change being that the setConfiguration function has changed on the cache adapter interface to be setContext.

We have written a custom adapter for the Laravel framework and our solution to handle this is going to be to implement both functions. This would allow our adapter to interoperate with both versions of the package.

In 1.1.0, our setConfiguration call was empty and did nothing, but it seems the new setContext has a specific intent.
It seems the changes are intended to clean up the cache after the time windows have expired. If we have an empty setContext function would we still see the old behaviour?

intervalToHalfOpen not work for Redis storage

Redis storage adapter does not implements saveLastFailureTime,and loadLastFailureTime suppose the key is failureKey, instead of lastFailureKey, so always got null value.

Change Ackintosh\Ganesha\Storage lastFailureKey as following works:

    /**
     * @param  string $service
     * @return string
     */
    private function lastFailureKey($service)
    {
        return $this->supportRollingTimeWindow() 
            ? $this->failureKey($service) 
            : $this->prefix($service) . self::KEY_SUFFIX_LAST_FAILURE_TIME;
    }

Creating new strategies

Hi,

Great package 🙌

We'd like to create a new strategy that opens the circuit if a request takes too long for example if a request took longer then 5s X times in X seconds. One way to achieve this is to set the timeout in Guzzle/http client which would throw an exception but the problem with this is that we want the request to continue to process rather than throw an exception after a certain timeout.
Therefore we we're thinking of creating a new Strategy that would track the time it took to process and either say success/failure based on this time.

The problem we have is how we set the Configuration for this timeout because this gets passed to the service but the configuration for this Strategy will be different to the existing ones. Any plans/thoughts on how to change the configuration for new strategies?

Request to Add FailureDetectorInterface to GuzzleMiddleware in 2.x Version

We are currently using Guzzle for HTTP requests in our project and rely on its middleware functionality. We have noticed that the latest ganesha 3.x version introduced a highly useful feature, which is support for custom error detection functions. However, the 3.x version requires PHP 8 or higher to install, and we are currently running PHP 7.4, which does not support PHP 8 immediately.

We kindly request the author to consider backporting the FailureDetectorInterface to the 2.x version of ganesha . This would greatly benefit users like us who are unable to upgrade to PHP 8 immediately but still want to leverage this feature. It would allow us to enhance our error handling capabilities while maintaining compatibility with our existing PHP version.

Thank you for your attention to this matter, and we appreciate your consideration of our request.

Call to undefined function GuzzleHttp\Promise\promise_for()

Version : ackintosh/ganesha 2.0.2
Middleware : Guzzle

After upgrading guzzlehttp/promises from 1.5.3 to 2.0.1 on my project, I get a :
Fatal error: Uncaught Error: Call to undefined function GuzzleHttp\Promise\promise_for() in /home/web/sfc/vendor/ackintosh/ganesha/src/Ganesha/GuzzleMiddleware.php:52

And indeed, GuzzleMiddleware needs guzzlehttp/promises functions that were removed in v2.0.

So guzzlehttp/promises dependency could probably be improved:

  • I think it's not possible to add guzzlehttp/promises in composer.json of ackintosh/ganesha, since it's only a "suggest".
  • But it could perhaps be handled by creating a ganesha-middleware-guzzle library that would have a dependency on guzzlehttp/promises ?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.