Getting around API timeouts with Cloud Functions and Cloud Pub/Sub
Tags: serverless, cloudfunctions, cloudpubsub
Published: 2020-05-20 16:21:49

Recently I was pitched a revolutionary Slackbot idea. What if, with the power of Cloud Functions and Cloud Vision, I could take any image and [dogeify](https://en.wikipedia.org/wiki/Doge_(meme)) it?

<figure>
  <img src="https://dev-to-uploads.s3.amazonaws.com/i/3mdm0ei781hhzi5e8ray.png" width="400">
  <figcaption><em>An image run through my Slackbot</em></figcaption>
</figure>

In this post, I'll show you how I built a Slackbot with Python that does exactly this. In addition, I'll show you how to overcome a common issue when working with APIs and how a unique Cloud Functions pattern can help you overcome this obstacle.


## The timeout problem

My [slash command](https://api.slack.com/interactivity/slash-commands) Slackbot consisted of one Cloud Function, doge-response. When someone types `/doge` in their Slack client, Slack sends an HTTP event that triggers my Cloud Function with the text the user typed after my command. This text should be an image URL to dogeify.


<img src="https://dev-to-uploads.s3.amazonaws.com/i/kzlo92i90uvgvyioula8.png" width="400">

The data flow looked like this:

<img src="https://dev-to-uploads.s3.amazonaws.com/i/ker1wliqi24rmuole3c0.png" width="600">


Unfortunately, when it was time for testing, my Slackbot would think about things for a bit and return an **operation_timeout** error.

<figure>
  <img src="https://dev-to-uploads.s3.amazonaws.com/i/5vuiur5omknumutb4orx.png" width="400">
  <figcaption><em>Ugh.</em></figcaption>
</figure>

Timeouts are a problem for many services. Some give you 5 seconds, some even more, but in the end it sometimes isn’t enough for what you want to do.

After some research, I learned that Slack only gives you **[three seconds](https://api.slack.com/interactivity/slash-commands#responding_basic_receipt)** to respond to a slash command. Three seconds isn’t enough time to run a large image though Cloud Vision, create a new image, overlay Cloud Vision labels, and upload that image to Cloud Storage.

How did I get around these pesky timeouts? The solution is to get asynchronous, which wasn’t as hard as I thought it would be. By sending messages between multiple Cloud Functions, I was able to avoid API timeouts entirely.


## Two Cloud Functions are better than one

The first step towards an asynchronous Slackbot was to split my single Cloud Function into two. 

To facilitate messaging between these two functions, I tied in [Cloud Pub/Sub](https://cloud.google.com/pubsub/docs/). Cloud Pub/Sub is a real-time messaging service that allows you to send and receive messages between independent apps.

<img src="https://dev-to-uploads.s3.amazonaws.com/i/xu2w6a5qqwl6qz4nzfnj.png" width="600">


**Cloud Function 1: doge-queue**

[Triggered by HTTP](https://cloud.google.com/functions/docs/calling/http) from `/doge` slash Slack command.
1. Verifies that the request is good
2. Publishes the contents of the Slack response to my Cloud Pub/Sub topic, **doge-convert**
3. Returns a “Working on that” message back to the Slack channel

**Cloud Function 2: doge-response**

[Triggered by Cloud Pub/Sub](https://cloud.google.com/functions/docs/calling/pubsub) publishings to the **doge-convert** topic.
1. Decodes the Cloud Pub/Sub event data and now has the contents of the Slack request
2. Dogefies the image
3. Sends the image back to the slack channel where it was called using the **response_url** attribute of the Slack request

In the **doge-queue** Cloud Function, publishing one message to Cloud Pub/Sub takes less than 3 seconds, giving me time to send a response back to the Slack channel.

<figure>
  <img src="https://dev-to-uploads.s3.amazonaws.com/i/12ej1yh1xq8ope401l4e.png" width="400">
  <figcaption><em>Much better than a timeout!</em></figcaption>
</figure>

This message buys me **30 minutes** to respond back to the Slack channel. While 30 minutes is awesome, I also need to think about other timeouts that could arise. Cloud Functions itself has a default timeout of **one minute**, which is plenty of time for what I need to do. If I needed more time, I could have [updated my Cloud Function’s timeout](https://cloud.google.com/functions/docs/concepts/exec#timeout) to be up to **9 minutes**. 


## Gettin’ Cloud Pub/Sub with it

After setting up a my Cloud Pub/Sub **doge-convert** topic and creating a [pull subscription](https://cloud.google.com/pubsub/docs/subscriber#push_pull) for **doge-response**, it was time to connect the dots between my two Cloud Functions.

In my `doge_queue` function, I dumped the contents of the Slack request into a JSON object.

`publisher_client.publish()` then takes my topic path and the JSON object (encoded), publishing it to my topic.

By assigning `publisher_client.publish()` to a variable, `future`, I can check the `future.result` to confirm the message was published successfully.


{% gist https://gist.github.com/abbycar/78e96d8ac275af363fdfdff4aef0a593 file=main.py %}


At this point, a message sent with my Slackbot is now in my doge-convert topic. Once that message hits the topic, doge-response is triggered by the event.

In my `doge_response` function, I grab the `data` out of the event and decode it.

With just one line of code, I now have the contents of my Slack request to work with in my second Cloud Function.

{% gist https://gist.github.com/abbycar/35a1f966e35439290705d4757b7ee1e7 file=main.py %}



## Conclusion

And that’s it! By having my first Cloud Function send an initial message back to Slack while it publishes the contents of the Slack request to Cloud Pub/Sub, I buy my second Cloud Function plenty of time to do all the heavy lifting. Goodbye 3 second API timeout limitation!

If you want to see all the code for this Slackbot, including how I handled image storage, called the Cloud Vision API, and handled text overlay, check out my GitHub repo!
{% github abbycar/doge-a-chat no-readme %}