Firebase HTTPS函数 – 在完成后台工作之前以200状态代码响应

huangapple go评论61阅读模式
英文:

Firebase HTTPS Functions - Responding with a 200 status code Before Completing Background Work

问题

我们正在使用HTTPS请求来触发云函数,以执行以下操作:

  1. 为了避免来自调用者的更多重试,快速响应(<10秒)并返回200响应。
  2. 然后在结束函数之前进行两个其他POST(时间敏感)请求。

总体而言,有哪些最佳解决方案来处理这个问题?我们已经看到了以下解决方案:

  1. PubSub 具有订阅者,在发送了我们的200响应后异步完成POST请求。
  2. 将云任务加入队列 到任务队列。我们不确定这些是否会受到HTTPS终止 的影响,就像我们在尝试中看到的那样。
  3. 使用基于事件的触发器,例如Cloud Firestore,写入带有某个ID的文档,触发onCreate事件以完成POST请求。

其他相关细节是,由于我们是一个较小的应用程序(远远少于1000 QPS),现在的流量很小,可能排除了Pub Sub,我们希望在<1-2秒内创建后台任务,以保持良好的用户体验(消息应用程序)。

最初,我们的代码如下:

exports.example = functions
  .https
  .onRequest(async (request, response) => {
    const authHeader = request.get("authorization");
    const { statusCode, httpsResponseMessage } =
      verifyAuthHeader(authHeader, token);
    // Send 200 response to our caller within 10 seconds to avoid more retries.
    response.status(statusCode).send(
     {
      message: httpsResponseMessage,
     }
    );
    // do other POST requests
  }

我们还尝试了:

exports.example = functions
  .https
  .onRequest(async (request, response) => {
    const authHeader = request.get("authorization");
    const { statusCode, httpsResponseMessage } =
      verifyAuthHeader(authHeader, token);
    // Send 200 response to our caller within 10 seconds to avoid more retries.
    response.write(
      JSON.stringify(
        {
          message: httpsResponseMessage
        }
      )
    );
    // do other POST requests
    res.end()
  }

但是注意到我们的代码在response.status(statusCode).send()之后的不可预测行为,如Firebase文档的提示和技巧部分所述,并且使用response.write发送部分结果不会避免来自调用者的未来重试。

英文:

We are triggering a cloud function with a HTTPS request to:

  1. Respond quickly (&lt;10s) to the caller with a 200 Response to avoid more retries from our caller.
  2. THEN make two other POST (time-sensitive) requests before ending the function.

In general, what are the best solutions to deal with this problem? We have seen:

  1. PubSub to have subscribers complete the POST requests async after sending off our 200 response.
  2. Enqueue Cloud Tasks to a task queue. We are not sure if these are affected by the HTTPS termination like we have seen with our attempt approaches.
  3. Using an Event Based Trigger like Cloud Firestore to write to a document with some id which triggers an onCreate event to finish out the POST requests.

Other relevant details are that we have small traffic now as a smaller app (much less than 1000 QPS which may rule out Pub Sub) and would like to target creating the background task in &lt;1-2 seconds to still have a good user experience (messaging app).

Originally, we had our code like:

exports.example = functions
  .https
  .onRequest(async (request, response) =&gt; {
    const authHeader = request.get(&quot;authorization&quot;);
    const {statusCode, httpsResponseMessage} =
      verifyAuthHeader(authHeader, token);
    // Send 200 response to our caller within 10 seconds to avoid more retries.
    response.status(statusCode).send(
     {
      message: httpsResponseMessage,
     }
    );
    // do other POST requests
  }

We also tried:

exports.example = functions
  .https
  .onRequest(async (request, response) =&gt; {
    const authHeader = request.get(&quot;authorization&quot;);
    const {statusCode, httpsResponseMessage} =
      verifyAuthHeader(authHeader, token);
    // Send 200 response to our caller within 10 seconds to avoid more retries.
    response.write(
      JSON.stringify(
        {
          message: httpsResponseMessage
        }
      )
    );
    // do other POST requests
    res.end()
  }

But noticed unpredictable behavior with our code AFTER response.status(statusCode).send() as noted in the tips & tricks section of Firebase docs and also that sending a partial result with response.write does not avoid future retries from the caller.

答案1

得分: 3

你需要非常小心处理HTTP函数,并确保在触发响应之前,所有工作都已完成。这在Node.js运行时文档中有详细说明:

当处理涉及到回调或Promise对象的异步任务时,您必须明确告知运行时您的函数已经完成了这些任务的执行。您可以使用不同的方式来实现这一点,如下面的示例所示。关键是,您的代码必须等待异步任务或Promise完成后才能返回;否则,您的函数的异步组件可能在完成之前被终止。

// 正确:等待Promise完成后发送HTTP响应
await Promise.resolve();

// 错误:HTTP函数应该发送一个
// HTTP响应而不是返回。
return Promise.resolve();

// HTTP函数应该通过返回HTTP响应来信号终止。
// 这不应该在所有后台任务完成之前执行。
res.send(200);
res.end();

// 错误:这可能不会执行,因为
// 已经发送了HTTP响应。
return Promise.resolve();

实际操作中,这意味着您的两个示例永远不会按预期工作,因为您在完成工作之前就已经触发了响应,如// do other POST requests所述。

根据您的描述,听起来:

  1. 您需要快速响应调用者。
  2. 当调用此函数时,您需要触发另外两个函数。
  3. 但是来自另外两个函数的响应不会用于原始调用者的响应。

如果是这样的情况,我建议您使用Pub/Sub触发的后台函数,并按照以下方式组织此函数:

const { PubSub } = require('@google-cloud/pubsub')

const pubSubTopic1 = new PubSub('your-project-id').topic('the-first-topic-name')
const pubSubTopic2 = new PubSub('your-project-id').topic('the-second-topic-name')

exports.example = functions
  .https
  .onRequest(async (request, response) => {
    const authHeader = request.get("authorization");
    const { statusCode, httpsResponseMessage } = verifyAuthHeader(authHeader, token);

    // 发布消息到Pub/Sub主题以触发那些函数
    await Promise.all([
      pubSubTopic1.publishMessage('message-1'),
      pubSubTopic2.publishMessage('message-2')
    ]);

    // 在10秒内向调用者发送200响应,以避免更多的重试。
    response.status(statusCode).send({
      message: httpsResponseMessage,
    });

    // 在调用 .send() 后什么都不做,因为函数已经终止
  }

Pub/Sub具有低延迟和快速的特点,鉴于您处理的并发量,您的函数应该保持在热状态并快速启动。

不要使用Firestore来实现此功能;您将因为创建文档并且触发函数而遇到成本问题,您将因为创建大量文档而遇到成本问题,您将不得不保留或删除这些文档,并且每次创建/删除操作都会产生成本。而且不管怎样,Firestore文档触发器的工作方式是通过发送Pub/Sub消息,因此在您的流程中引入文档写入只会增加整个流程的延迟。

示例PubSub函数

正如下面的评论中所提到的,使用firebase-functions会很重且笨重,因此这里提供了一个不需要它的示例PubSub触发的函数。

假设此函数期望接收作为消息发送到PubSub主题的JSON字符串。例如,在上面的HTTP函数中,您可以像这样发布一条消息:

const message1 = {
  "foo": "bar",
  "bar": "baz"
}
let statusCode = 202 // 已接受

try {
  await pubSubTopic1.publishMessage({ json: message1 })
} catch (error) {
  statusCode = 502 // 网关错误
} finally {
  res.status(statusCode).send({ message: httpsResponseMessage })
}

您接收的PubSub函数可以非常简单,如下所示:

export const examplePubSubFunction = message => {
  const data = JSON.parse(Buffer.from(message.data, 'base64').toString())
  console.log(data.foo) // 打印 "bar"
  console.log(data.bar) // 打印 "baz"
}

然后,您可以使用以下命令部署该函数:

gcloud functions deploy examplePubSubFunction --runtime=nodejs18 --trigger-topic=the-first-topic-name
英文:

You need to be very careful with HTTP functions and ensure that all your work is complete before you initiate the response. This is documented in The Node.js Runtime:

> When working with asynchronous tasks that involve callbacks or Promise objects, you must explicitly inform the runtime that your function has finished executing these tasks. You can do this in several different ways, as shown in the samples below. The key is that your code must wait for the asynchronous task or Promise to complete before returning; otherwise the asynchronous component of your function may be terminated before it completes.
>
>
&gt; // OK: await-ing a Promise before sending an HTTP response
&gt; await Promise.resolve();
&gt;
&gt; // WRONG: HTTP functions should send an
&gt; // HTTP response instead of returning.
&gt; return Promise.resolve();
&gt;
&gt; // HTTP functions should signal termination by returning an HTTP response.
&gt; // This should not be done until all background tasks are complete.
&gt; res.send(200);
&gt; res.end();
&gt;
&gt; // WRONG: this may not execute since an
&gt; // HTTP response has already been sent.
&gt; return Promise.resolve();
&gt;

In practice, this means that your two examples will never work as expected because you are initiating the response before you complete the work as described by // do other POST requests.

From your description it sounds like:

  1. You need to respond quickly to the caller
  2. You need to fire two other functions when calls are made to this function
  3. But the responses from the other two functions are not used in the response to the original caller

If that is the case then I recommend that you use Pub/Sub-triggered background functions and structure this function as follows:

const { PubSub } = require(&#39;@google-cloud/pubsub&#39;)

const pubSubTopic1 = new PubSub(&#39;your-project-id&#39;).topic(&#39;the-first-topic-name&#39;)
const pubSubTopic2 = new PubSub(&#39;your-project-id&#39;).topic(&#39;the-second-topic-name&#39;)

exports.example = functions
  .https
  .onRequest(async (request, response) =&gt; {
    const authHeader = request.get(&quot;authorization&quot;);
    const {statusCode, httpsResponseMessage} = verifyAuthHeader(authHeader, token);

    // Publish messages to the Pub/Sub topics to trigger those functions
    await Promise.all([
      pubSubTopic1.publishMessage(&#39;message-1&#39;),
      pubSubTopic2.publishMessage(&#39;message-2&#39;)
    ]);

    // Send 200 response to our caller within 10 seconds to avoid more retries.
    response.status(statusCode).send(
     {
      message: httpsResponseMessage,
     }
    );

    // Do nothing after calling .send() because the function has terminated
  }

Pub/Sub is fast with low latency and given the amount of concurrency you're handling your functions should remain in a warm state and fire quickly.

Don't use Firestore for this feature; you'll run into cost issues by creating a document and firing a function, and you'll run into cost issues by creating a huge number of documents that you'll either have to retain or delete, and every create/delete operation incurs cost. And regardless, the way Firestore document triggers work is by sending a Pub/Sub message, so introducing a document write in your process is only going to add latency to the overall process.


Example PubSub function

As mentioned in the comments below using firebase-functions is heavy and clunky so here is an example PubSub-triggered function that does not require it.

Assume that this function expects to receive a JSON string as the message sent to the PubSub topic. For example, in the HTTP function above you might publish a message like this:

const message1 = {
  &quot;foo&quot;: &quot;bar&quot;,
  &quot;bar&quot;: &quot;baz&quot;
}
let statusCode = 202 // Accepted

try {
  await pubSubTopic1.publishMessage({ json: message1 })
} catch (error) {
  statusCode = 502 // Bad Gateway
} finally {
  res.status(statusCode).send({ message: httpsResponseMessage })
}

Your receiving PubSub function can be as small and simple as this:

export const examplePubSubFunction = message =&gt; {
  const data = JSON.parse(Buffer.from(message.data, &#39;base64&#39;).toString())
  console.log(data.foo) // prints &quot;bar&quot;
  console.log(data.bar) // prints &quot;baz&quot;
}

You can then deploy the function with:

gcloud functions deploy examplePubSubFunction --runtime=nodejs18 --trigger-topic=the-first-topic-name

huangapple
  • 本文由 发表于 2023年6月29日 02:34:30
  • 转载请务必保留本文链接:https://go.coder-hub.com/76575848.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定