Search code examples
laravelwebsocketreactphp

ReactPHP - Working with Laravel and Timers


So here is a very interesting problem I am having. I am dead set on trying to figure out how to integrate a websocket client into Laravel 5.5 to allow communication over a websocket between my application and a Discord Gateway. I spooled up a new Laravel app and required this library, via composer, which is built on top of Ratchet PHP.

I am attempting to build a PHP Discord Bot after this bot that i use to use but was abandon after a major dependency it uses was discontinued as well.

I have figured out how to add a timer in in order to send heartbeats like this

$app->addTimer(x, function ($thing) use ($etc) {});

This works perfectly until my app receives an event from Discord. Then the synchronization is lost and instead of sending a heartbeat at the determined interval form the hello event, my app starts sending them every 3 - 9 seconds and sometimes 2 or three at a time. Here is some output from the console for debugging reason, but it shows my problem:

"Sending Heartbeat - 41 - 2017-12-31 05:26:42"
"Sending Heartbeat - 41 - 2017-12-31 05:26:42"
Illuminate\Support\Collection {#679
  #items: array:4 [
    "t" => null
    "s" => null
    "op" => 11
    "d" => null
  ]
}
Illuminate\Support\Collection {#679
  #items: array:4 [
    "t" => null
    "s" => null
    "op" => 11
    "d" => null
  ]
}
"Sending Heartbeat - 41 - 2017-12-31 05:26:51"
Illuminate\Support\Collection {#602
  #items: array:4 [
    "t" => null
    "s" => null
    "op" => 11
    "d" => null
  ]
}
"Sending Heartbeat - 41 - 2017-12-31 05:26:55"
Illuminate\Support\Collection {#695
  #items: array:4 [
    "t" => null
    "s" => null
    "op" => 11
    "d" => null
  ]
}
"Sending Heartbeat - 41 - 2017-12-31 05:26:56"
Illuminate\Support\Collection {#688
  #items: array:4 [
    "t" => null
    "s" => null
    "op" => 11
    "d" => null
  ]
}
Illuminate\Support\Collection {#688
  #items: array:4 [
    "t" => "TYPING_START"
    "s" => 7
    "op" => 0
    "d" => array:3 [
      "user_id" => "277968564827324416"
      "timestamp" => 1514698064
      "channel_id" => "394991263344230411"
    ]
  ]
}
Illuminate\Support\Collection {#688
  #items: array:4 [
    "t" => "MESSAGE_CREATE"
    "s" => 8
    "op" => 0
    "d" => array:15 [
      "type" => 0
      "tts" => false
      "timestamp" => "2017-12-31T05:27:47.057000+00:00"
      "pinned" => false
      "nonce" => "396897209817235456"
      "mentions" => []
      "mention_roles" => []
      "mention_everyone" => false
      "id" => "396897202448105494"
      "embeds" => []
      "edited_timestamp" => null
      "content" => "!about"
      "channel_id" => "394991263344230411"
      "author" => array:4 [
        "username" => "David Davaham"
        "id" => "277968564827324416"
        "discriminator" => "2471"
        "avatar" => "0c27e1bed49121e8aaf3f284d6b74e55"
      ]
      "attachments" => []
    ]
  ]
}
Illuminate\Support\Collection {#688
  #items: array:4 [
    "t" => "MESSAGE_CREATE"
    "s" => 9
    "op" => 0
    "d" => array:15 [
      "type" => 0
      "tts" => false
      "timestamp" => "2017-12-31T05:27:49.382000+00:00"
      "pinned" => false
      "nonce" => null
      "mentions" => array:1 [
        0 => array:4 [
          "username" => "David Davaham"
          "id" => "277968564827324416"
          "discriminator" => "2471"
          "avatar" => "0c27e1bed49121e8aaf3f284d6b74e55"
        ]
      ]
      "mention_roles" => []
      "mention_everyone" => false
      "id" => "396897212199862276"
      "embeds" => []
      "edited_timestamp" => null
      "content" => "<@!277968564827324416> Unfortunately That is not a command I recognize. Please try again. Reply with `!help` for a list of commands"
      "channel_id" => "394991263344230411"
      "author" => array:5 [
        "username" => "Claire Underwood (Dev)"
        "id" => "394988052360986635"
        "discriminator" => "8397"
        "bot" => true
        "avatar" => null
      ]
      "attachments" => []
    ]
  ]
}
"Sending Heartbeat - 41 - 2017-12-31 05:27:12"
Illuminate\Support\Collection {#616
  #items: array:4 [
    "t" => null
    "s" => null
    "op" => 11
    "d" => null
  ]
}
"Sending Heartbeat - 41 - 2017-12-31 05:27:23"
"Sending Heartbeat - 41 - 2017-12-31 05:27:23"
Illuminate\Support\Collection {#622
  #items: array:4 [
    "t" => null
    "s" => null
    "op" => 11
    "d" => null
  ]
}
Illuminate\Support\Collection {#622
  #items: array:4 [
    "t" => null
    "s" => null
    "op" => 11
    "d" => null
  ]
}
"Sending Heartbeat - 41 - 2017-12-31 05:27:32"
Illuminate\Support\Collection {#652
  #items: array:4 [
    "t" => null
    "s" => null
    "op" => 11
    "d" => null
  ]
}
"Sending Heartbeat - 41 - 2017-12-31 05:27:36"
Illuminate\Support\Collection {#667
  #items: array:4 [
    "t" => null
    "s" => null
    "op" => 11
    "d" => null
  ]
}
"Sending Heartbeat - 41 - 2017-12-31 05:27:37"
Illuminate\Support\Collection {#661
  #items: array:4 [
    "t" => null
    "s" => null
    "op" => 11
    "d" => null
  ]
}
"Sending Heartbeat - 41 - 2017-12-31 05:27:46"
Illuminate\Support\Collection {#663
  #items: array:4 [
    "t" => null
    "s" => null
    "op" => 11
    "d" => null
  ]
}
"Sending Heartbeat - 41 - 2017-12-31 05:27:49"
Illuminate\Support\Collection {#660
  #items: array:4 [
    "t" => null
    "s" => null
    "op" => 11
    "d" => null
  ]
}
"Sending Heartbeat - 41 - 2017-12-31 05:27:51"
Illuminate\Support\Collection {#656
  #items: array:4 [
    "t" => null
    "s" => null
    "op" => 11
    "d" => null
  ]
}
"Sending Heartbeat - 41 - 2017-12-31 05:27:53"
Illuminate\Support\Collection {#698
  #items: array:4 [
    "t" => null
    "s" => null
    "op" => 11
    "d" => null
  ]
}
"Sending Heartbeat - 41 - 2017-12-31 05:28:04"
"Sending Heartbeat - 41 - 2017-12-31 05:28:04"
Illuminate\Support\Collection {#701
  #items: array:4 [
    "t" => null
    "s" => null
    "op" => 11
    "d" => null
  ]
}
Illuminate\Support\Collection {#701
  #items: array:4 [
    "t" => null
    "s" => null
    "op" => 11
    "d" => null
  ]
}
"Sending Heartbeat - 41 - 2017-12-31 05:28:13"
Illuminate\Support\Collection {#706
  #items: array:4 [
    "t" => null
    "s" => null
    "op" => 11
    "d" => null
  ]
}
"Sending Heartbeat - 41 - 2017-12-31 05:28:17"
Illuminate\Support\Collection {#707
  #items: array:4 [
    "t" => null
    "s" => null
    "op" => 11
    "d" => null
  ]
}
"Sending Heartbeat - 41 - 2017-12-31 05:28:18"
Illuminate\Support\Collection {#709
  #items: array:4 [
    "t" => null
    "s" => null
    "op" => 11
    "d" => null
  ]
}

Here is the code:

$conn->on('message', function(MessageInterface $msg) use ($conn, $socket, $loop) {
    $message = collect(json_decode($msg, true));
    dump($message);
    if ($message->has('s') && $message->get('s') !== null) {
        $this->seq = $message->get('s');
    }
    if (!$this->is_ready) {
        if (!$message->has('op')) {
            $conn->close();
        }
        if ($message->get('op') == 0) {
            if ($message->get('t') === "READY" || $message->get('t') === "GUILD_CREATE") {
                $this->is_ready = true;
                $this->seq = $message->get('s');
            }
        }
        if ($message->get('op') == 10) {
            $this->connOpened = Carbon::now();
            $this->heartbeat = (int)floor($message->get('d')['heartbeat_interval'] / 1000);
            $socket->sendIdentify();
            sleep(1);
        }
        if ($message->get('op') == 11) {
            $now = Carbon::now()->timestamp;
            if (!$this->heartbeatACK) {
                $this->heartbeatACK = true;
            }
        }
    }
    if ($this->is_ready) {
        if ($message->get('op') == 0) {
            if ($message->get('t') === "MESSAGE_CREATE") {
                $trigger = config('discord.message.trigger');
                $data = $message->get('d');
                $msgContent = $data['content'];
                if (starts_with($msgContent, $trigger)) {
                    ProcessMessage::dispatch($data);
                }
            }
        }
        $now = Carbon::now();
        $loop->addTimer($this->heartbeat, function ($x) use ($now, $conn, $socket) {
            dump("Sending Heartbeat - " . $this->heartbeat . " - " .$now->toDateTimeString());
            $payload = collect([
                'op' => 1,
                'd' => (int)$this->seq,
            ]);
            $conn->send($payload->toJson());
        });
    }

});

Does anybody know could way of better managing the heartbeats or is this just something that I will need to tolerate?

Also, does anybody have an advice or criticism for how I am doing this? I could not find any reliable documentation on how to do this, so I am kind of piecing it together as I am going.


Solution

  • So this has been resolved. Here is what i found:

    With the code outlined about, when the following block of code was executed:

    if (!$this->is_ready) {
        ...
        if ($message->get('op') == 0) {
            if ($message->get('t') === "READY" || $message->get('t') === "GUILD_CREATE") {
                $this->is_ready = true;
                $this->seq = $message->get('s');
            }
        }
        ...
    }
    

    followed up by this snippet since $this->is_ready was being set to true, it would cause two timers to be added. In addition to this, because I had the code for is_ready like this:

    if ($this->is_ready) {
         if ($message->get('op') == 0) {
             if ($message->get('t') === "MESSAGE_CREATE") {
                 $trigger = config('discord.message.trigger');
                 $data = $message->get('d');
                 $msgContent = $data['content'];
                 if (starts_with($msgContent, $trigger)) {
                     ProcessMessage::dispatch($data);
                 }
             }
         }
         $now = Carbon::now();
         $loop->addTimer($this->heartbeat, function ($x) use ($now, $conn, $socket) {
             dump("Sending Heartbeat - " . $this->heartbeat . " - " .$now->toDateTimeString());
             $payload = collect([
                 'op' => 1,
                 'd' => (int)$this->seq,
             ]);
             $conn->send($payload->toJson());
         });
     }
    

    everytime I received a new event, another timer was being scheduled, causing the timers to stack up and a heartbeat to be sent when those timers expired. This resulted in timers eventually being sent multiple times per second, if I would have of left it like this. The following are the changes I made:

    I added a timer to the first block here like this:

    if ($message->get('op') == 0) {
        if ($message->get('t') === "READY" || $message->get('t') === "GUILD_CREATE") {
            $this->is_ready = true;
            $loop->addTimer(20, function ($x) use ($conn,$loop) {
                dump("Sending Heartbeat - " . $this->heartbeat . " - " .Carbon::now()->toDateTimeString());
                $payload = collect([
                    'op' => 1,
                    'd' => (int)$this->seq,
                ]);
                $conn->send($payload->toJson());
                $loop->cancelTimer($x);
            });
        }
    }
    

    this causes the loop the stop to process this timer, but at the bottom, I cancel the timer AFTER I send the heartbeat. I am not sure if this is important since it is just a timer and not a periodicTimer, but I don't think it hurts.

    Next I restructured the is_ready if statement like this:

    if ($this->is_ready) {
        if ($message->get('op') == 0) {
            if ($message->get('t') === "MESSAGE_CREATE") {
                $trigger = config('discord.message.trigger');
                $data = $message->get('d');
                $msgContent = $data['content'];
                if (starts_with($msgContent, $trigger)) {
                    ProcessMessage::dispatch($data);
                }
            }
        } else {
            $now = Carbon::now();
            $loop->addTimer(20, function ($x) use ($conn,$loop) {
                dump("Sending Heartbeat - " . $this->heartbeat . " - " .Carbon::now()->toDateTimeString());
                $payload = collect([
                    'op' => 1,
                    'd' => (int)$this->seq,
                ]);
                $conn->send($payload->toJson());
                $loop->cancelTimer($x);
            });
        }
    }
    

    Notice that I am only scheduling a timer if the op code does NOT equal 0. I am relying on the fact that a previous heartbeat has been scheduled and will run. This is an almost guarantee, as long as the first one gets executed when the server is started.

    Here is the output from the console so far, and it has been running with this config for about five or ten minutes with no double heartbeats, or anything.

    "Sending Heartbeat - 41.25 - 2017-12-31 08:41:18"
    Illuminate\Support\Collection {#702
      #items: array:4 [
        "t" => null
        "s" => null
        "op" => 11
        "d" => null
      ]
    }
    "Sending Heartbeat - 41.25 - 2017-12-31 08:41:38"
    Illuminate\Support\Collection {#704
      #items: array:4 [
        "t" => null
        "s" => null
        "op" => 11
        "d" => null
      ]
    }
    "Sending Heartbeat - 41.25 - 2017-12-31 08:41:58"
    Illuminate\Support\Collection {#706
      #items: array:4 [
        "t" => null
        "s" => null
        "op" => 11
        "d" => null
      ]
    }
    "Sending Heartbeat - 41.25 - 2017-12-31 08:42:18"
    Illuminate\Support\Collection {#708
      #items: array:4 [
        "t" => null
        "s" => null
        "op" => 11
        "d" => null
      ]
    }
    "Sending Heartbeat - 41.25 - 2017-12-31 08:42:39"
    Illuminate\Support\Collection {#710
      #items: array:4 [
        "t" => null
        "s" => null
        "op" => 11
        "d" => null
      ]
    }
    Illuminate\Support\Collection {#712
      #items: array:4 [
        "t" => "PRESENCE_UPDATE"
        "s" => 7
        "op" => 0
        "d" => array:6 [
          "user" => array:1 [
            "id" => "277968564827324416"
          ]
          "status" => "idle"
          "roles" => []
          "nick" => null
          "guild_id" => "394991263344230409"
          "game" => null
        ]
      ]
    }
    "Sending Heartbeat - 41.25 - 2017-12-31 08:42:59"
    Illuminate\Support\Collection {#712
      #items: array:4 [
        "t" => null
        "s" => null
        "op" => 11
        "d" => null
      ]
    }
    "Sending Heartbeat - 41.25 - 2017-12-31 08:43:19"
    Illuminate\Support\Collection {#717
      #items: array:4 [
        "t" => null
        "s" => null
        "op" => 11
        "d" => null
      ]
    }
    

    If you have any questions, please feel free to reach out to me. This code will eventually be committed to a public repo on my Github if you want to check it out. For now, the repo is private. Check my profile for a link to my GitHub.