Highly scalable Discord bot on Google Cloud, part 4: Three Cloud Functions and two PUBSUB topics

........
.     

This post is part of a series of posts which experiment with building a highly scalable Discord bot which reacts to the "/mando" command by generating a unique mandala and serving the image back, implemented using multiple approaches. 

I did this to play and experiment with GCP. Might want to read the introductive post first. I've experimented with a single serverless function and it's not easy to implement what we want. Two serverless functions and a PUBSUB topic does the job well. For the next experiment, we are going to use another serverless function to post the reply back to Discord. Using this approach we can scale independently the function which generates the mandala. For example, we can allocate more CPU and memory for it. As it is more expensive to execute this function, it's important not to use it for mundane tasks, like send a PATCH request back to Discord. However, this requires an extra PUBSUB topic. For a production system you need to carefully consider the costs associated to the extra topic and the new serverless function vs the costs related to running a more resource heavy function for a bit longer. For this experiment, I'm going to assume it's cheaper to introduce a new function and a new topic.





TLDR
  • We decided to allocate more CPU and memory to the function which generates the mandala. Doing so, it becomes inefficient to use this resource heavy function for sending HTTP replies back to Discord, so we extracted one more serverless function to send the data back to Discord. We had to introduce a new PUBSUB topic to store data on the new generated mandala.

Deploying the new PUBSUB topic

gcloud pubsub topics create reply-data
The result:


raw-discord-data function

Exactly the same as described in this post, nothing changed.

image-generator function

The update here is that the Discord reply is no longer sent in this function. The HTTP PATCH invocation was replaced with post data to the new PUBSUB topic.

Source code:
main.py
[....]
def publish_to_pub_sub(discord_url, data):
    client = pubsub_v1.PublisherClient()
    topic_path = client.topic_path(project_name, reply_data_topic_name)
    pubsub_data = {
        "discord_url": discord_url,
        "data": data,
    }
    data_bytes = json.dumps(pubsub_data).encode("utf-8")
    future = client.publish(topic_path, data=data_bytes)


[....]

def post_mando_result(interaction_token, mando_filename):
    mando_url = f"https://storage.googleapis.com/{bucket_name}/{mando_filename}"
    discord_url = f"https://discord.com/api/v8/webhooks/{discord_app_id}/{interaction_token}/messages/@original"
    data = {
        "tts": False,
        "content": "image",
        "embeds": [
            {
                "thumbnail": {"url": mando_url},
                "image": {"url": mando_url},
            }
        ],
        "allowed_mentions": {"parse": []},
    }
    publish_to_pub_sub(discord_url, data)


def handle_request(event, context):
    pubsub_message = base64.b64decode(event["data"]).decode("utf-8")
    msg_json = json.loads(pubsub_message)
    local_file_name = f"{str(uuid.uuid4())}.png"
    create_mando(local_tmp_folder=local_tmp_folder, local_file_name=local_file_name)
    upload_blob(f"{local_tmp_folder}/{local_file_name}", local_file_name)
    interaction_token = msg_json["interaction_token"]
    post_mando_result(
        interaction_token=interaction_token,
        mando_filename=local_file_name,
    )
    return {}, 200

Notice that the "post_mando_result" function no longer does the PATCH call, but invokes the "publish_to_pub_sub" method which post the webhook Discord url and the data to the "reply-data" topic.

discord-reply function

This new serverless function doesn't do much. It's invoked by new messages posted on the "reply-data" topic and it simply does a HTTP PATCH on the webhook URL provided by the Discord server.

Source code:
main.py

import base64
import json
import requests

def reply_mando_result(discord_url, data):
    reply_response = requests.patch(
        url=discord_url,
        json=data,
    )
    reply_response.raise_for_status()

def handle_request(event, context):
    pubsub_message = base64.b64decode(event["data"]).decode("utf-8")
    msg_json = json.loads(pubsub_message)
    discord_url = msg_json["discord_url"]
    data = msg_json["data"]

    reply_mando_result(
        discord_url=discord_url,
        data=data,
    )
    return {}, 200

Comments