Developers & Practitioners

Avoiding GCF anti-patterns part 3: How to establish outbound connections correctly

Editor's note: Over the next several weeks, you'll see a series of blog posts focusing on best practices for writing Google Cloud Functions based on common questions or misconceptions as seen by the Support team.  We refer to these as "anti-patterns" and offer you ways to avoid them.  This article is the third post in the series.

Scenario

Suppose you have a Cloud Function that makes some sort of outbound connection in response to a trigger event, e.g. your Function makes a HTTP(s) POST request, and your Function fails with a 500 error in the logs.

Most common root issue

The Function did not set an explicit timeout limit when making the outbound connection call and the Cloud Function exhausted its execution time. We see customers configure their overall Function timeout time from the default 60s to the max 540s, but they do not put a limit on their outbound connection calls. 

How to investigate

You need to determine "what is my use case?" Do you want to have a lower error rate or lower latency?

If having a lower error rate is more important, you can take additional steps to achieve this by configuring your Function to have the max timeout of 540s and manually retry the outbound connection while Function execution time allows. Note that if you are using Google APIs, you do not need to implement your own retry logic. Our connections have logic for retries built in by default. For example, for Cloud Storage, you can learn more about the default retry strategy and possible configurations. But if you are using a 3rd party library that does not have built-in retries, you'd have to make that determination on how to handle errors. One possibility is to use Cloud Tasks. For example, if the outbound request you're making is a "fire and forget" scenario (i.e. you're not planning to take any action based on the response) you can use Cloud Tasks to schedule the outbound request. Using this approach, you can rely on the Cloud Task retry semantics.

If having a low latency is more important, e.g. you need to optimize the user experience, you can "offload the call" by sending an appropriate response to the caller and have another Cloud Function handle the request by using PubSub or Cloud Tasks. For example, you have a chatbot app where a user can request a copy of their utility bill. The chatbot can trigger a Cloud Function that makes an outbound call to retrieve the download link. Suppose that the customer asks for a copy of the utility bill that you know will take a long time to generate (e.g. the utility bill alongside a detailed usage report with cost-savings tips), the Cloud Function can use Cloud Task to generate the download link and email the customer when it is ready. The Cloud Function can respond to the chatbot to tell the customer that their detailed report will be emailed to them when it is ready. 

How to offload the outbound connection call

Below is an example of how to use a Cloud Function (e.g. the Function responding to the chatbot) to create a Cloud Task to invoke a HTTP Cloud Function to generate a download link for the utility bill. Note for this example, you'll need to make additional configurations, specifically configuring the Cloud Task queue and service account permissions. See the "Other Helpful Tips" section. 

Below is the code for the Cloud Function that could (in theory) interact with the chat bot. The following Function "createABackgroundTask" creates a Cloud Task and specifies a service account that can invoke the Function.

  // Imports the Google Cloud Tasks library.
const { CloudTasksClient } = require("@google-cloud/tasks");
 
// Instantiates a client.
const client = new CloudTasksClient();
 
exports.createABackgroundTask = async (req, res) => {
 const email = "<sa-to-invoke-functions-from-tasks>@<PROJECT_ID>.iam.gserviceaccount.com";
 const project = "<PROJECT-ID>";
 const queue = "my-queue";
 const location = "us-central1";
 const url = "https://<REGION>-<PROJECT-ID>.cloudfunctions.net/generateDownloadLink";
 const payload = { message: "customer details here" };
 const formattedParent = client.queuePath(project, location, queue);
 const task = {
   httpRequest: {
     httpMethod: "POST",
     url: url,
     headers: {
       "Content-Type": "application/json"
     },
     oidcToken: {
       serviceAccountEmail: email,
       audience: new URL(url)
     }
   }
 };
 
 if (payload) {
   task.httpRequest.body = Buffer.from(JSON.stringify(payload)).toString("base64");
 }
 
 console.log("Sending task:");
 console.log(task);
 const request = {
   parent: formattedParent,
   task: task
 };
 const [response] = await client.createTask(request);
 console.log(`Created task ${response.name}`);
 res.status(200).send(`We will email you with a link to download your bill.`);
};

Below is the code for the Cloud Function that is triggered by the Task. In this example, the "generateDownloadLink" is an authenticated Function.

  exports.generateDownloadLink = (req, res) => {
 console.log("Received request for: ", req.body);
 // do the actual work here
 res.status(200).send("successfully processed request");
};

Other helpful tips

  • This tutorial shows you how to use a Cloud Task to trigger a Cloud Function to send a scheduled email. It'll also walk you through creating a Cloud Task queue and setting up a service account that will invoke the Function from Cloud Task. By specifying a service account for the Task, you can use an authenticated Function.

  • If you're using a different service account for your "createABackgroundTask" Function's identity than the default, you need to verify that the service account has permissions to create Tasks. It will need the Cloud Tasks Enqueuer role roles/cloudtasks.enqueuer.