How do I... Use long-polling?
If your system uses long-polling, then your bots will have to long-poll too, to create an accurate test. You have two ways of doing this.
Blocking
The simplest way to long-poll is to repeatedly make HTTP requests (with an appropriately long timeout). You could do this in a recursive loop, or as a repeating action. For example:
# infinite loop long-poll action
def long_poll(context) do
_response = HTTPoison.get!(@long_poll_url, @headers, timeout: 300)
# recur
long_poll(context)
end
While this effectively represents long-polling, it presents a problem - the bot can't do anything else. The bot is either waiting for a response, or starting a new request. The bot will never get a chance to perform any other actions.
This probably isn't what you want, though it can be useful for performance metrics. For example, split up your bots to have half busy long-polling, and the other half making writes. This won't be an accurate load on your system, but it can give you useful metrics.
Non-blocking
For a more accurate representation, where bots can both long-poll and do their normal routine at the same time, you need to set up a concurrent asynchronous long-polling mechanic.
The way to do this in Elixir is to create another Erlang process, which is simple and light-weight. The idea is to kick off the recursive long-poll in a separate process. Each time it gets a response, it needs to update the bot, then poll again. Because the bot's process isn't tied up waiting for a response, it can do other things.
The part that I glossed over is how the long-polling process "updates" the bot. Each
bot is actually a GenServer
, so the long-polling process can call
it with
update messages. The bot will process the message in between actions, giving you a
chance to update the context.
In order to handle the incoming message, you need to extend your bot with a custom
BotArmy.Bot
. Depending on how you set it up, you can also include logic to switch
between different feeds, as well as conditionally stop a specific feed. Here's an
example:
# In MyActions.ex
@doc """
Action to kick off a long-polling process for the active feed.
Only call this action once to initialize the process. If the active feed changes,
you should call this again with the new feed id.
Make sure to handle the update messages in a custom `BotArmy.Bot`.
"""
def start_long_polling(%{active_feed_id: feed_id}) when is_binary(feed_id) do
bot_pid = self()
long_poll_url = @url_base <> "/feeds/#{feed_id}"
headers = []
# This starts a new process, and does not block the bot. Since it is linked to the
# bot, if either process crashes, the other one will too. In this case, if the
# long-poll request errors, the bot will die.
spawn_link(fn ->
# wrap the long-polling steps in a function so it can recur
do_long_poll = fn ->
# Make the long-poll, blocking until it returns
%{body: body, status_code: 200} = HTTPoison.get!(long_poll_url, headers, timeout: 300)
# Send the response to the bot, blocking until the bot handles it and replies.
# The message is a tuple that we need to create a `handle_call` for in our custom
# bot. In this case, the bot's reply is a boolean for if we should poll again.
continue_polling? =
GenServer.call(bot_pid, {:long_poll_response, feed_id, Jason.parse!(body)})
if continue_polling? do
# recur
do_long_poll()
else
# exiting with `:normal` will quit the polling process without quitting the bot
exit(:normal)
end
end
# kick off the initial long-poll
do_long_poll()
end)
end
# In MyBot.ex
# Be sure to specify this custom bot when running your test
defmodule MyBot do
@moduledoc """
Extended `BotArmy.Bot` to handle long-polling updates.
See https://hexdocs.pm/bot_army/1.0.0/BotArmy.Bot.html#module-extending-the-bot
"""
# You need this line to get all of the base `Bot` functionality
use BotArmy.Bot
@doc """
This matches the message sent from a long-polling process. It receives the
response from the poll and replies with a boolean whether the long-polling should
continue or not, based on if the active feed changed since the long-poll started.
"""
@impl GenServer
def handle_call({:long_poll_response, feed_id, response}, _from, context) do
# Note that `context` is the same state of the bot used in the actions.
# Make sure the response is for a feed that we still care about
if feed_id == context.active_feed_id do
# update the context however you need to based on the long-poll response
new_context = Map.put(context, :feed_data, response)
{:reply, true, new_context}
else
# The active feed changed, so we ignore the obsolete response and reply not to
# continue polling.
{:reply, false, context}
end
end
end