{"id":9730,"date":"2024-05-21T13:42:16","date_gmt":"2024-05-21T13:42:16","guid":{"rendered":"https:\/\/www.ntspl.co.in\/blog\/?p=9730"},"modified":"2024-08-20T11:54:37","modified_gmt":"2024-08-20T11:54:37","slug":"making-api-calls-exactly-once-when-using-workflows","status":"publish","type":"post","link":"https:\/\/www.ntspl.co.in\/blog\/making-api-calls-exactly-once-when-using-workflows\/","title":{"rendered":"Making API calls exactly once when using Workflows"},"content":{"rendered":"<div class=\"block-paragraph_advanced\">\n<h2><strong>Introduction<\/strong><\/h2>\n<p>One challenge with any distributed system, including <a href=\"https:\/\/cloud.google.com\/workflows\">Workflows<\/a>, is ensuring that requests sent from one service to another are processed exactly once, when needed; for example, when placing a customer order in a shipping queue, withdrawing funds from a bank account, or processing a payment.<\/p>\n<p>In this blog post, we\u2019ll provide an example of a website invoking Workflows, and Workflows in turn invoking a <a href=\"https:\/\/cloud.google.com\/functions\">Cloud Function<\/a>. We\u2019ll show how to make sure both Workflows and the Cloud Function logic only runs once. We\u2019ll also talk about how to invoke Workflows exactly once when using <a href=\"https:\/\/cloud.google.com\/blog\/topics\/developers-practitioners\/introducing-workflows-callbacks\">HTTP callbacks<\/a>, <a href=\"https:\/\/cloud.google.com\/pubsub\">Pub\/Sub<\/a> messages, or <a href=\"https:\/\/cloud.google.com\/tasks\">Cloud Tasks<\/a>.<\/p>\n<h2><strong>Invoke Workflows exactly once<\/strong><\/h2>\n<p>Imagine you have an online store and you\u2019re using Workflows to create new orders, save to Firestore, and process payments by calling a Cloud Function:<\/p>\n<\/div>\n<div class=\"block-image_full_width\">\n<div class=\"article-module h-c-page\">\n<div class=\"h-c-grid\">\n<figure class=\"article-image--large h-c-grid__col h-c-grid__col--6 h-c-grid__col--offset-3 \"><img decoding=\"async\" src=\"https:\/\/storage.googleapis.com\/gweb-cloudblog-publish\/images\/image1_hVpeh5J.max-1000x1000.png\" alt=\"image1\" \/><\/figure>\n<\/div>\n<\/div>\n<\/div>\n<div class=\"block-paragraph_advanced\">\n<p>A new customer order comes in, the website makes an API call to Workflows but receives an error. Two possible scenarios are:<\/p>\n<p>(1) The request is lost and the workflow is never invoked:<\/p>\n<\/div>\n<div class=\"block-image_full_width\">\n<div class=\"article-module h-c-page\">\n<div class=\"h-c-grid\">\n<figure class=\"article-image--medium h-c-grid__col h-c-grid__col--4 h-c-grid__col--offset-4 \"><img decoding=\"async\" src=\"https:\/\/storage.googleapis.com\/gweb-cloudblog-publish\/images\/image2_1VaigE8.max-1000x1000.png\" alt=\"image2\" \/><\/figure>\n<\/div>\n<\/div>\n<\/div>\n<div class=\"block-paragraph_advanced\">\n<p>(2) The workflow is invoked and executes successfully, however the response is lost:<\/p>\n<\/div>\n<div class=\"block-image_full_width\">\n<div class=\"article-module h-c-page\">\n<div class=\"h-c-grid\">\n<figure class=\"article-image--medium h-c-grid__col h-c-grid__col--4 h-c-grid__col--offset-4 \"><img decoding=\"async\" src=\"https:\/\/storage.googleapis.com\/gweb-cloudblog-publish\/images\/image3_Lfq82pR.max-1000x1000.png\" alt=\"image3\" \/><\/figure>\n<\/div>\n<\/div>\n<\/div>\n<div class=\"block-paragraph_advanced\">\n<p>How can you make sure the workflow executes once?<\/p>\n<p>To solve this, the website retries the same request. One easy solution is to check if a document already exists in Firestore:<\/p>\n<\/div>\n<div class=\"block-code\">\n<dl>\n<dt>code_block<\/dt>\n<dd>&lt;ListValue: [StructValue([(&#8216;code&#8217;, &#8216;main:rn params: []rn steps:rn &#8211; init:rn assign:rn &#8211; project_id: ${sys.get_env(&#8220;GOOGLE_CLOUD_PROJECT_ID&#8221;)}rn &#8211; order_id: &#8220;12345&#8221; # In practice we would pass in the order ID as a workflow parameter, e.g. ${params[0]}rn &#8211; firestore_collection: &#8220;orders&#8221;rn &#8211; URL: https:\/\/us-central1-&lt;your_project_id&gt;.cloudfunctions.net\/processpaymentrn &#8211; create_document:rn try:rn call: googleapis.firestore.v1.projects.databases.documents.createDocumentrn args:rn collectionId: ${firestore_collection}rn parent: ${&#8220;projects\/&#8221; + project_id + &#8220;\/databases\/(default)\/documents&#8221;}rn query:rn documentId: ${order_id}rn except:rn as: ern steps:rn &#8211; endEarly:rn return: ${e} # Exception is raised, e.g. ${e.code == 409} if doc already existsrn &#8211; processPayment:rn &#8230;&#8217;), (&#8216;language&#8217;, &#8221;), (&#8216;caption&#8217;, &lt;wagtail.rich_text.RichText object at 0x3e81cd479e50&gt;)])]&gt;<\/dd>\n<\/dl>\n<\/div>\n<div class=\"block-paragraph_advanced\">\n<p>The <code>processPayment <\/code>step will execute only if a document is successfully created. This is effectively a 1-bit state machine, idempotent, and a valid solution. The downside of this solution is that it\u2019s not extensible. We might want to complete additional work in this handler before changing states, or expand the number of states within the system. Next, let\u2019s continue with a more advanced solution for the same problem.<\/p>\n<h2><strong>Invoke Cloud Functions from Workflows exactly once<\/strong><\/h2>\n<p>Let\u2019s see what happens when the workflow uses a Cloud Function to process the payment. You might have the following step to call Cloud Functions:<\/p>\n<\/div>\n<div class=\"block-code\">\n<dl>\n<dt>code_block<\/dt>\n<dd>&lt;ListValue: [StructValue([(&#8216;code&#8217;, &#8216;- processPayment:rn call: http.postrn args:rn url: https:\/\/us-central1-&lt;your_project_id&gt;.cloudfunctions.net\/processpaymentrn auth:rn type: OIDC&#8217;), (&#8216;language&#8217;, &#8221;), (&#8216;caption&#8217;, &lt;wagtail.rich_text.RichText object at 0x3e81cd479c70&gt;)])]&gt;<\/dd>\n<\/dl>\n<\/div>\n<div class=\"block-paragraph_advanced\">\n<p>By default, Workflows offers <strong>at-most-once<\/strong> delivery (no retries) with HTTP requests. That\u2019s usually OK because 99.9+% of the time, the call is successful, and a response is received.<\/p>\n<p>In the rare case of failure, a <a href=\"https:\/\/cloud.google.com\/workflows\/docs\/reference\/syntax\/error-types#error-tags\"><code>ConnectionError<\/code><\/a> might be raised. As in the website-to-workflow situation discussed previously, the workflow can\u2019t tell which scenario occurred. Similarly, you can add retries.<\/p>\n<p>Let\u2019s add a <a href=\"https:\/\/cloud.google.com\/workflows\/docs\/reference\/syntax\/retrying#default-retry-policy\">default retry policy<\/a> to handle this:<\/p>\n<\/div>\n<div class=\"block-code\">\n<dl>\n<dt>code_block<\/dt>\n<dd>&lt;ListValue: [StructValue([(&#8216;code&#8217;, &#8220;- processPayment:rn try:rn call: http.postrn args:rn url: https:\/\/us-central1-&lt;your_project_id&gt;.cloudfunctions.net\/processpaymentrn auth:rn type: OIDCrn retry: ${http.default_retry} # Retries up to 5 times, includes &#8216;ConnectionError'&#8221;), (&#8216;language&#8217;, &#8221;), (&#8216;caption&#8217;, &lt;wagtail.rich_text.RichText object at 0x3e81cd85cbb0&gt;)])]&gt;<\/dd>\n<\/dl>\n<\/div>\n<div class=\"block-paragraph_advanced\">\n<p>Let&#8217;s say the second delivery scenario occurs where the request is received by the Cloud Function but the response is lost. By adding retries, Workflows will likely invoke the Cloud Function multiple times. When this happens, how do you ensure that the code in the Cloud Function only runs once?<\/p>\n<p>You\u2019ll need to add extra logic to the Cloud Function to check and update the payment state in Firestore:<\/p>\n<\/div>\n<div class=\"block-image_full_width\">\n<div class=\"article-module h-c-page\">\n<div class=\"h-c-grid\">\n<figure class=\"article-image--large h-c-grid__col h-c-grid__col--6 h-c-grid__col--offset-3 \"><img decoding=\"async\" src=\"https:\/\/storage.googleapis.com\/gweb-cloudblog-publish\/images\/image4_HtrYAfU.max-1000x1000.png\" alt=\"image4\" \/><\/figure>\n<\/div>\n<\/div>\n<\/div>\n<div class=\"block-paragraph_advanced\">\n<p>Let\u2019s also assume you want to track the workflow <code>EXECUTION_ID<\/code> in Firestore and use the following <code>order_state <\/code>enum to allow for additional flexibility in payment processing:<\/p>\n<\/div>\n<div class=\"block-code\">\n<dl>\n<dt>code_block<\/dt>\n<dd>&lt;ListValue: [StructValue([(&#8216;code&#8217;, &#8216;payment_not_processed \/\/ Initial state when an order is createdrnpayment_declined \/\/ Payment was not successfulrnpayment_successful \/\/ Payment processed successfullyrn&#8230;&#8217;), (&#8216;language&#8217;, &#8221;), (&#8216;caption&#8217;, &lt;wagtail.rich_text.RichText object at 0x3e81cd9cbbb0&gt;)])]&gt;<\/dd>\n<\/dl>\n<\/div>\n<div class=\"block-paragraph_advanced\">\n<p>You can expand on the previous workflow and call a Cloud Function to process the payment:<\/p>\n<\/div>\n<div class=\"block-code\">\n<dl>\n<dt>code_block<\/dt>\n<dd>&lt;ListValue: [StructValue([(&#8216;code&#8217;, &#8216;main:rn params: []rn steps:rn &#8211; init:rn assign:rn &#8211; project_id: ${sys.get_env(&#8220;GOOGLE_CLOUD_PROJECT_ID&#8221;)}rn &#8211; order_id: &#8220;12345&#8221; # In practice we would pass in the order ID as a workflow parameter, e.g. ${params[0]}rn &#8211; firestore_collection: &#8220;orders&#8221;rn &#8211; URL: https:\/\/us-central1-&lt;your_project_id&gt;.cloudfunctions.net\/processpaymentrn &#8211; create_document:rn try:rn call: googleapis.firestore.v1.projects.databases.documents.createDocumentrn args:rn collectionId: ${firestore_collection}rn parent: ${&#8220;projects\/&#8221; + project_id + &#8220;\/databases\/(default)\/documents&#8221;}rn query:rn documentId: ${order_id}rn body:rn fields:rn order_state: # We set an initial statern stringValue: &#8220;payment_not_processed&#8221;rn workflow_id: # And also track this workflow execution IDrn stringValue: ${sys.get_env(&#8220;GOOGLE_CLOUD_WORKFLOW_EXECUTION_ID&#8221;)}rn except:rn as: ern steps:rn &#8211; endEarly:rn return: ${e} # Exception is raised, e.g. ${e.code == 409} if doc already existsrn &#8211; processPayment:rn try:rn call: http.postrn args:rn url: ${URL} # Might get called multiple times!rn auth:rn type: OIDCrn body:rn order_id: ${order_id}rn result: rrn retry: ${http.default_retry}rn &#8211; returnStep:rn return: ${r}&#8217;), (&#8216;language&#8217;, &#8221;), (&#8216;caption&#8217;, &lt;wagtail.rich_text.RichText object at 0x3e81cd47faf0&gt;)])]&gt;<\/dd>\n<\/dl>\n<\/div>\n<div class=\"block-paragraph_advanced\">\n<p>Here\u2019s the Cloud Function (Node.js v20) that processes the payment:<\/p>\n<\/div>\n<div class=\"block-code\">\n<dl>\n<dt>code_block<\/dt>\n<dd>&lt;ListValue: [StructValue([(&#8216;code&#8217;, &#8216;const functions = require(&#8216;@google-cloud\/functions-framework&#8217;);rnconst firestore = require(&#8216;@google-cloud\/firestore&#8217;);rnrnrnfunctions.http(&#8216;helloHttp&#8217;, (req, res) =&gt; {rn const fs = new firestore.Firestore();rn try{rn\/\/ Reads the current state from Firestore and updates it within the same transaction to make this handler idempotent. Using a transaction is important. Note: It could be run multiple times but will only be committed once.rn return fs.runTransaction(t =&gt; {rn const docRef = fs.doc(&#8220;orders\/&#8221; + req.body.order_id);rn return t.get(docRef).then(doc =&gt; {rn console.log(doc, &#8216;=&gt;&#8217;, doc);rn var state = doc.data().order_statern \/\/ Only process the order if we haven&#8217;t alreadyrn if (state == &#8220;payment_not_processed&#8221;) {rn \/\/ Do payment stuff, e.g. debit account from another Firestore documentrn \/\/ &#8230;rn \/\/rn state = &#8220;payment_successful&#8221;rn t.update(docRef, {order_state: state})rn res.status(200).send(state);rn returnrn }rn res.status(200).send(&#8220;request ignored, state already: &#8221; + state);rn });rn }).then(result =&gt; {rn console.log(&#8216;Transaction result: &#8216;, result);rn });rn } catch (e) {rn console.log(&#8216;Transaction failure:&#8217;, e);rn } rn});&#8217;), (&#8216;language&#8217;, &#8221;), (&#8216;caption&#8217;, &lt;wagtail.rich_text.RichText object at 0x3e81cd47f550&gt;)])]&gt;<\/dd>\n<\/dl>\n<\/div>\n<div class=\"block-paragraph_advanced\">\n<p><code>package.json<\/code><\/p>\n<\/div>\n<div class=\"block-code\">\n<dl>\n<dt>code_block<\/dt>\n<dd>&lt;ListValue: [StructValue([(&#8216;code&#8217;, &#8216;{rn &#8220;dependencies&#8221;: {rn &#8220;@google-cloud\/functions-framework&#8221;: &#8220;^3.3.0&#8221;,rn &#8220;@google-cloud\/firestore&#8221;: &#8220;^7.6.0&#8243;rn }rn}&#8217;), (&#8216;language&#8217;, &#8221;), (&#8216;caption&#8217;, &lt;wagtail.rich_text.RichText object at 0x3e81cd514fa0&gt;)])]&gt;<\/dd>\n<\/dl>\n<\/div>\n<div class=\"block-paragraph_advanced\">\n<p>The key takeaway is that all payment processing work occurs within a <strong>transaction<\/strong>, making all actions idempotent. The code within the transaction might run multiple times due to Workflows retries, but it\u2019s only committed once.<\/p>\n<h2><strong>What about HTTP callbacks, Pub\/Sub, Cloud Tasks?<\/strong><\/h2>\n<p>So far, we\u2019ve talked about how to make website-to-workflow and Workflows to Cloud Functions requests, exactly once. There are other ways of invoking or resuming Workflows such as <a href=\"https:\/\/cloud.google.com\/blog\/topics\/developers-practitioners\/introducing-workflows-callbacks\">HTTP callbacks<\/a>, Pub\/Sub messages or Cloud Tasks. How do you make those requests exactly once? Let\u2019s take a look.<\/p>\n<h3><strong>Callbacks<\/strong><\/h3>\n<p>The good news is that Workflows HTTP callbacks are fully idempotent by default. It\u2019s safe to retry a callback if it fails. For example:<\/p>\n<\/div>\n<div class=\"block-code\">\n<dl>\n<dt>code_block<\/dt>\n<dd>&lt;ListValue: [StructValue([(&#8216;code&#8217;, &#8216;- createCallbackStep:rn call: events.create_callback_endpointrn args:rn http_callback_method: &#8220;POST&#8221;rn result: callback_detailsrn- sendOutURL:rn call: http.postrn args:rn url: &#8220;https:\/\/your-endpoint.com\/foo&#8221;rn body:rn callback_to_use: ${callback_details.url}rn&#8230;rn- callbackWaitStep:rn call: events.await_callbackrn args:rn callback: ${callback_details}&#8217;), (&#8216;language&#8217;, &#8221;), (&#8216;caption&#8217;, &lt;wagtail.rich_text.RichText object at 0x3e81cd48a100&gt;)])]&gt;<\/dd>\n<\/dl>\n<\/div>\n<div class=\"block-paragraph_advanced\">\n<p>Let\u2019s assume that the first callback returns an error to the external caller. Based on the error, the caller might not know if the workflow callback was received, and should retry the callback. On the second callback, the caller will receive one of the following HTTP status codes:<\/p>\n<ul>\n<li>\n<p role=\"presentation\"><strong>429<\/strong> indicates that the first callback was received successfully. The second callback is rejected by the workflow.<\/p>\n<\/li>\n<li>\n<p role=\"presentation\"><strong>200<\/strong> indicates that the second callback was received successfully. The first callback was either never received, or was received and processed successfully. If the latter, the second callback is not processed because <code>await_callback <\/code>is called only once. The second callback is discarded at the end of the workflow.<\/p>\n<\/li>\n<li>\n<p role=\"presentation\"><strong>404<\/strong> indicates that a callback is not available. Either the first callback was received and processed and the workflow has completed, or the workflow is not running (and has failed, for example). To confirm this, you\u2019ll need to send an API request to query the workflow execution state.<\/p>\n<\/li>\n<\/ul>\n<p>For more details, see <a href=\"https:\/\/cloud.google.com\/workflows\/docs\/creating-callback-endpoints#invoke-once\">Invoke a workflow exactly once using callbacks<\/a>.<\/p>\n<h3><strong>Pub\/Sub messages\u00a0<\/strong><\/h3>\n<p>When using Pub\/Sub to <a href=\"https:\/\/cloud.google.com\/workflows\/docs\/trigger-workflow-eventarc\">trigger<\/a> a new workflow execution, Pub\/Sub uses <strong>at-least-once<\/strong> delivery with Workflows, and will retry on any delivery failure. Pub\/Sub messages are automatically deduplicated. You don\u2019t need to worry about duplicate deliveries in that time window (24 hours).<\/p>\n<h3><strong>Cloud Tasks<\/strong><\/h3>\n<p>Cloud Tasks is commonly used to <a href=\"https:\/\/cloud.google.com\/workflows\/docs\/tutorials\/buffer-workflows-executions\">buffer workflow executions<\/a> and provides <strong>at-least-once <\/strong>delivery but it <strong>doesn\u2019t<\/strong> have message deduplication. Workflow handlers <a href=\"https:\/\/cloud.google.com\/tasks\/docs\/dual-overview\">should be idempotent<\/a>.<\/p>\n<h2><strong>Conclusion<\/strong><\/h2>\n<p>Exactly-once request processing is a hard problem. In this blog post, we\u2019ve outlined some scenarios where you might need exactly-once request processing when you\u2019re using Workflows. We also provided some ideas on how you can implement it. The exact solution will depend on the actual use case and the services involved.<\/p>\n<\/div>\n","protected":false},"excerpt":{"rendered":"<p>Introduction One challenge with any distributed system, including Workflows, is ensuring that requests sent from one service to another are processed exactly once, when needed; for example, when placing a customer order in a shipping queue, withdrawing funds from a bank account, or processing a payment. In this blog post, we\u2019ll provide an example of [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":9838,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[438],"tags":[],"class_list":["post-9730","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-technology"],"acf":{"custom_meta_title":"Ensure Single API Calls with Workflows: Best Practices","meta_description":"Learn the best practices for making API calls precisely once within Workflows. Optimize efficiency and streamline processes with expert guidance.","meta_keyword":"","other_meta_tag":"<meta property=og:type content=\"article\" \/>\r\n<meta property=og:title content=\"Making API calls exactly once when using Workflows\"\/>\r\n<meta property=og:description content=\"Learn the best practices for making API calls precisely once within Workflows. Optimize efficiency and streamline processes with expert guidance.\"\/>\r\n<meta property=\"og:image\" content=\"https:\/\/www.ntspl.co.in\/blog\/wp-content\/uploads\/2024\/05\/Making-API-calls-exactly-once-when-using-Workflows.jpg\"\/>\r\n<meta property=og:url content=\"https:\/\/www.ntspl.co.in\/blog\/making-api-calls-exactly-once-when-using-workflows\/\"\/>\r\n<meta property=og:site_name content=NTSPL \/>\r\n<meta name=\"twitter:site\" content=\"@NTSPL\">\r\n<meta name=twitter:card content=\"summary\" \/>\r\n<meta name=twitter:description content=\"Learn the best practices for making API calls precisely once within Workflows. Optimize efficiency and streamline processes with expert guidance.\"\/>\r\n<meta name=twitter:title content=\"Making API calls exactly once when using Workflows\"\/>"},"_links":{"self":[{"href":"https:\/\/www.ntspl.co.in\/blog\/wp-json\/wp\/v2\/posts\/9730"}],"collection":[{"href":"https:\/\/www.ntspl.co.in\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.ntspl.co.in\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.ntspl.co.in\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.ntspl.co.in\/blog\/wp-json\/wp\/v2\/comments?post=9730"}],"version-history":[{"count":6,"href":"https:\/\/www.ntspl.co.in\/blog\/wp-json\/wp\/v2\/posts\/9730\/revisions"}],"predecessor-version":[{"id":10378,"href":"https:\/\/www.ntspl.co.in\/blog\/wp-json\/wp\/v2\/posts\/9730\/revisions\/10378"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.ntspl.co.in\/blog\/wp-json\/wp\/v2\/media\/9838"}],"wp:attachment":[{"href":"https:\/\/www.ntspl.co.in\/blog\/wp-json\/wp\/v2\/media?parent=9730"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.ntspl.co.in\/blog\/wp-json\/wp\/v2\/categories?post=9730"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.ntspl.co.in\/blog\/wp-json\/wp\/v2\/tags?post=9730"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}