reactphp / reactphp

Event-driven, non-blocking I/O with PHP.

Home Page:https://reactphp.org

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

State-Management Error - Memory leak

Parsoolak opened this issue · comments

commented

Hi dear lovely community

I Have a problem with my ReactPHP Project.
I Have Created an application to save/update crypto price every second in Redis and use it on my another related project.

I use a state-Manager variable to avoid Re-Running at same time.
but now i have a problem! My State-Manager Variable Get stuck!

I Also Add Promise-Timer to my project but it still stop!
I use last version of reactPHP-reactMySQL-reactRedis-reactTimeout.

Here is My Code:

// Setup EventLoop
$loop = React\EventLoop\Loop::get();

// Database Factory
$databaseFactory = new Factory($loop);

// Async Redis
$redisFactory = new Clue\React\Redis\Factory();
$asyncRedis = $redisFactory->createLazyClient(// My Cred);

//  Database
$database = new DatabaseHelper($databaseFactory->createLazyConnection(// My Cred));

// Create Feed Generator // Thats Generate My Feed
$feedGenerator = new FeedGenerator();

// Do Update
$isActive = false;
$loop->addPeriodicTimer(1, function () use ($feedGenerator, $asyncRedis, &$isActive) {
    Logger::console(date("Y-m-d H:i:s") . " | Start Running | " . intval($isActive));

    if ($isActive) {
        Logger::console(date("Y-m-d H:i:s") . " | Error | Service Already Active");
        return false;
    }

    $isActive = true;
    return timeout($feedGenerator->getCryptoFeed(), 5)
        ->then(function ($result) use ($asyncRedis, &$isActive) {
            if (!$result['result']) {
                $isActive = false;
                Logger::console(date("Y-m-d H:i:s") . " | Error | Generating | {$result['error']}");
                return false;
            }

            return $asyncRedis->set("CryptoPrices", json_encode($result['content']))
                ->then(function ($result) use (&$isActive) {
                    $isActive = false;
                    if ($result !== 'OK') {
                        Logger::console(date("Y-m-d H:i:s") . " | Error | Redis | {$result}");
                        return false;
                    } else {
                        Logger::console(date("Y-m-d H:i:s") . " | Success");
                        return true;
                    }
                });
        }, function ($error) use (&$isActive) {
            $isActive = false;
            if ($error instanceof TimeoutException) {
                Logger::console(date("Y-m-d H:i:s") . " | Exception | Timed-Out | Restore State");
            } else {
                Logger::console(date("Y-m-d H:i:s") . " | Exception | State Manager | Restore State");
            }
            return false;
        });
});

$loop->run();

and after 1-2 days Running it Stuck and something like this happeend:

Screen Shot 2022-02-15 at 9 54 16 PM copy

Please Help me fix this!
I have tried to fix it for 20 Days.
Its pleasure to hear your help @clue and other lovely community.

Hey @Parsoolak since you have a bunch of logging in there could you change it to include the memory usage at the time of logging? Something like:

Logger::console(date("Y-m-d H:i:s") . " | " . (memory_get_usage(false) / 1024 / 1024) . "MB | Start Running | " . intval($isActive));

Because currently it's hard to spot the baseline memory usage from your image, and by how much it creeps up every cycle.

And by judging that output, and I can only guess at best, something happens in FeedGenerator that causes failures. No clue what tho

commented

Hi Dear @WyriHaximus
I`v Logged the ram usage and now i have a report for you.

My codes got stuck at 19:55 and start increasing ram usage from 6 MB to 128 MB at 20:18 and my ram exhausted.
Also I wanna say something wierd! My code always stop on some exact time like 19:55 and 14:55 and 7:55 !!

Also i wanna attached that i use remote mysql server but i dont have any error logs there.

Screenshot of when my codes start stucking:
Screen Shot 2022-02-16 at 9 18 47 PM

Screenshot of when my codes ram exhausted:
Screen Shot 2022-02-16 at 9 19 14 PM

as you said it could be from FeedGenerator, Here it my codes on that part:

class FeedGenerator
{
    protected DatabaseHelper $db1;
    protected DatabaseHelper $db2
    protected DatabaseHelper $db3

    public function __construct(
        DatabaseHelper $db1,
        DatabaseHelper $db2,
        DatabaseHelper $db3)
    {
        $this->db1 = $db1;
        $this->db2 = $db2;
        $this->db3 = $db3;
    }

    public function getCryptoFeed(): PromiseInterface|Promise
    {
        return all([
            $this->db3->createSelectQuery("Market",['service' => 'IRT'],null,"ORDER BY date DESC LIMIT 30"),
            $this->db1->createSelectQuery("Cryptos",['isListed' => 1],null,"ORDER BY sarafRank ASC")
        ])->then(function ($results) {
            foreach ($results as $result) {
                if (!$result['result'])
                    return ['result' => false, 'error' => $result['error']];
                if ($result['count'] == 0)
                    return ['result' => false, 'error' => "Zero Count"];
            }

            $historicalTetherPrice = [];
            foreach ($results[0]['rows'] as $usdtRow) {
                $historicalTetherPrice[$usdtRow['date']] = doubleval($usdtRow['close']);
            }

            return map($results[1]['rows'], function ($cryptoRow) use ($historicalTetherPrice) {
                $symbol = $cryptoRow['symbol'];
                $name = $cryptoRow['name'];
                $exchanges = json_decode($cryptoRow['exchanges'], true);

                $pairedSymbol = $symbol . "-USDT";

                if ($symbol == "USDT")
                    return [
                        'result' => true,
                        's' => $symbol,
                        'n' => $name,
                        'h' => array_values($historicalTetherPrice),
                        'hu' => [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
                    ];

                if ($exchanges[0] != 'kucoin')
                    return ['result' => false];

                return $this->db2
                    ->createSelectQuery(
                        "Market",
                        ['pair' => $pairedSymbol],
                        null,
                        "ORDER BY date DESC LIMIT 30"
                    )->then(function ($result) use ($symbol, $name, $historicalTetherPrice) {
                        if (!$result['result'] || $result['count'] == 0)
                            return ['result' => false];

                        $historicalIrtPrice = [];
                        $historicalUsdPrice = [];

                        foreach ($result['rows'] as $priceRow) {
                            $historicalUsdPrice[] = GlobalHelper::internalRounder($priceRow['close']);

                            if (isset($historicalTetherPrice[$priceRow['date']])) {
                                $historicalIrtPrice[] = round($priceRow['close'] * $historicalTetherPrice[$priceRow['date']]);
                            } else {
                                $historicalIrtPrice[] = round($priceRow['close'] * array_values($historicalTetherPrice)[0]);
                            }
                        }

                        return [
                            'result' => true,
                            's' => $symbol,
                            'n' => $name,
                            'h' => $historicalIrtPrice,
                            'hu' => $historicalUsdPrice
                        ];

                    });
            })->then(function ($rows) {
                $finalRows = [];

                foreach ($rows as $row) {
                    if (!$row['result'])
                        continue;

                    $currentPrice = $row['hu'][0];

                    if (isset($row['h'][1]))
                        $changePercentage = round((($row['h'][0] - $row['h'][1]) / $row['h'][1]) * 100, 2);
                    else
                        $changePercentage = 0;

                    $irtPrice = $row['h'][0];
                    $pairWithName = "IRToman";

                    $finalRows[] = [
                        's' => $row['s'],
                        'n' => $row['n'],
                        'p' => $irtPrice,
                        'd' => $currentPrice,
                        'q' => $pairWithName,
                        'h' => $row['h'],
                        'hu' => $row['hu'],
                        'c' => $changePercentage
                    ];
                }

                return [
                    'result' => true,
                    'content' => [
                        'lastUpdateTime' => GlobalHelper::getCurrentMicroTime(),
                        'Image' => GlobalHelper::$cryptoAssetImageBaseEndpoint,
                        'Items' => $finalRows
                    ]
                ];

            });
        });
    } 

GlobalHelper::internalRounder is this:

    public static function internalRounder($price): float|int
    {
        ini_set('serialize_precision', 14);
        if ($price > 10000) {
            $precision = 0;
        } else if ($price > 1000) {
            $precision = 1;
        } else if ($price > 10) {
            $precision = 2;
        } else if ($price > 0.1) {
            $precision = 3;
        } else if ($price > 0.01) {
            $precision = 5;
        } else {
            $precision = 8;
        }
        return round($price, $precision, PHP_ROUND_HALF_DOWN);
    }

The Whole Process Get Crypto Market Rates From Exchanges and Get Internal-Country Price From a database and then merge them and calculate crypto prices in national Prices.

Much Thanks Dear Cees-Jan

There are not big weird things in there. That leads to two questions:

  1. What happens at XX55 every day?
  2. Why store everything in a single Redis key instead of multiple or a list? Because if there is a big burst of data you get more to process and might run out of memory
commented

I checked every service and nothing runs at XX55 But I think somehow I found the issue about your first question. every XX55 there's a release and renew happening from my DHCP client to renew the IP address..When this thing happens the whole timeout, MYSQL lazy connection and the react-mq stops working and it can't cancel the Promise nor Reject them...Whole applications stops working...

What I'm trying to say is that when the MYSQL Lazy Connection wants to create a new underlying connection for its own query queue this happens and fails the process of creating connection because of this exception other things fails too...

I mean maybe reactphp can't handle this type of exceptions!? or do you have any thoughts about these kind of situations...

Before diving into the what and why @Parsoolak. Do you get a new IP address every renew? If that is the case, your existing mysql/redis connection becomes invalid and the source/destination address changes. That isn't a ReactPHP issue perse, that is a networking issue.