What strategies can be employed to mitigate the activation of the losing arm in a Promise.race?

My current task involves sending the same query to multiple identical endpoints (about five) across various Kubernetes clusters. The goal is to aggregate the results without any delays and report failures to the user while continuing with the process seamlessly.

Here is an illustration of the expected output:

$ node find_broken_pods.js
tomato: error: timed out
Cluster  Pod         Reason
potato   bar-foos    Invalid PVC
potato   foo-eggnog  Invalid image
yoghurt  spam-baz    Invalid name

$ node find_broken_pods.js
Cluster  Pod         Reason
potato   bar-foos    Invalid PVC
potato   foo-eggnog  Invalid image
yoghurt  spam-baz    Invalid name
tomato   what-bat    Insufficient jQuery

During one iteration, we faced a timeout issue with the 'tomato' cluster, but successfully retrieved information from other clusters. In the next run, all details were fetched without any timeouts.

Initially, I developed the following solution:

export async function queryAll(): Promise<{cluster: string; deployments: V1Pod[]}[]> {
  const out: {cluster: string; result: V1Pod[]}[] = [];

  const promises: Promise<number>[] = [];
  for (const cluster of Object.values(CLUSTERS)) {
    promises.push(
      Promise.race([
        new Promise<number>((_, reject) => setTimeout(() => reject(new Error(`${cluster}: timed out`)), 5000)),
        new Promise<number>((resolve, _) =>
          getAllPods(cluster)
            .then(pods => resolve(out.push({cluster: cluster, result: pods})))
        ),
      ])
    );
  }

  await Promise.all(promises);

  return out;
}

Although this version executes tasks simultaneously, encountering a single failure leads to the entire function failing. To address this issue, I modified it as follows:

export async function queryAll(): Promise<{cluster: string; deployments?: V1Deployment[]; error?: string}[]> {
  const out: {cluster: string; result?: V1Pod[]; error?: string}[] = [];

  const promises: Promise<number>[] = [];
  for (const cluster of Object.values(CLUSTERS)) {
    promises.push(
      Promise.race([
        new Promise<number>((resolve, _) =>
          setTimeout(() => {
            resolve(out.push({cluster: cluster, error: 'timed out'}));
          }, 5000)
        ),
        new Promise<number>((resolve, _) =>
          getAllPods(cluster)
            .then(pods => resolve(out.push({cluster: cluster, result: pods})))
        ),
      ])
    );
  }

  await Promise.all(promises);

  return out;
}

Current observations show that both paths in the promise execute fully. This means that either all clusters provide data or none do, including timeouts:

  • If no timeout occurs, `Promise.all` does not wait for the `setTimeout`, although the Node process will delay its termination accordingly.
  • In case of any timeout, `Promise.all` awaits these events leading to all `setTimeout`s triggering.

I anticipated the losing portion in the `Promise.race` to terminate somehow or be prevented from executing. It seems like there are flaws in my approach... How can I enhance fault tolerance effectively?

Answer №1

Acknowledged, both tasks are currently active and pushing objects to the designated array. The execution of Promise.race does not reverse the actions performed to create the promises initially.

To prevent duplicate reporting from the loser of the race, consider implementing a flag for each cluster to indicate if the timeout or result has already been reported.

An alternative and simpler solution would be to directly utilize the result value that the promises resolve with, which is already facilitated by Promise.race:

interface QueryResult {cluster: string; deployments?: V1Deployment[]; error?: string}
export async function queryAll(): Promise<QueryResult[]> {
  const promises: Promise<QueryResult>[] = Object.values(CLUSTERS).map(cluster =>
    Promise.race([
      new Promise(resolve => {
        setTimeout(() => {
          resolve({cluster: cluster, error: 'timed out'});
        }, 5000)
      }),
      getAllPods(cluster).then(pods => ({cluster: cluster, result: pods}))
    ])
  );

  const out = await Promise.all(promises);

  return out;
}

This method ensures consistent results in the same order as the CLUSTERS, rather than based on their arrival sequence. If you prefer to maintain the original order of arrival, the initial approach utilizing push can still be used but with the result of each race!

export async function queryAll(): Promise<QueryResult[]> {
  const out: QueryResult[] = [];
  await Promise.all(Object.values(CLUSTERS).map(cluster =>
    Promise.race([
      new Promise(resolve => {
        setTimeout(() => {
          resolve({cluster: cluster, error: 'timed out'});
        }, 5000)
      }),
      getAllPods(cluster).then(pods => ({cluster: cluster, result: pods}))
    ]).then(result => {
      out.push(result);
    })
  ));
  return out;
}

Additionally, contemplate using Promise.allSettled instead of Promise.all - this allows for potential rejection from the timeout promise.

Answer №2

Seems like this solution is almost there, but not quite:

async function retrieveAllData(): Promise<{group: string; items: V1Pod[]}[]> {
  const data: {group: string; content: V1Pod[]}[] = [];

  const promisesList: Promise<number>[] = [];
  for (const group of Object.values(GROUPS)) {
    promisesList.push(
      Promise.race([
        new Promise<number>((_, reject) => setTimeout(() => reject(new Error(`${group}: request timed out`)), 5000)),
        new Promise<number>((resolve, _) =>
          fetchAllItems(group)
            .then(items => resolve(data.push({group: group, content: items})),
                  error => {
                    console.error(`${group}: ${error}\n`);
                    resolve(0);
              })
        ),
      ]).catch(error => {
        console.error(`${group}: ${error}\n`);
        return 0;
      })
    );
  }

  await Promise.all(promisesList);

  return data;
}

This could lead to the following output due to repeated error handling, which is acceptable to me; if I trigger a failure using /etc/hosts, I see:

$ node find_faulty_items.js
apple: Error: request timed out
Group    Item         Issue
banana   foo-bar      Invalid size
banana   bar-eggs     Invalid format
orange   spam-qux     Missing metadata
apple: Error: connect ECONNREFUSED 0.0.0.0:443

What concerns me more is that Node waited until the library threw an ECONNREFUSED error, even after my program had already finished generating its output; one of the reasons behind doing this is to handle scenarios where the process hangs waiting for a server response lost in firewall restrictions.

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

"Attempting to troubleshoot a calculator built with html, css, and js. I'm stumped as to what could

After following a tutorial on YouTube, going over the code multiple times, and still experiencing issues with the calculator. Sometimes it works fine, other times certain buttons like (+, -, x, ⁄) don't function properly. I've provided the enti ...

Issue: App is not being styled with Material UI Theme Colors

I'm having trouble changing the primary and secondary colors of Material UI. Even after setting the colors in the theme, the controls like Buttons or Fabs still use the default colors. Can someone help me figure out what I'm missing? index.js /* ...

retrieve all entries from a paginated grid

Currently, I am utilizing the datatable feature in bootstrap4 with a table that has pagination set to display 10 items per page. I have implemented a function to retrieve values from the table, however I am facing an issue where I am only able to retriev ...

Ways to guarantee the protection of APIs that are accessible to both front-end applications and other servers

In the process of creating a website, I am faced with the challenge of enabling the front-end page to communicate with the server using AJAX for data retrieval and posting. The same APIs offered by the server are also utilized by private apps within the ...

Having trouble with loading image textures in three.js

Here is the code snippet I am using: var scene = new THREE.Scene(); // adding a camera var camera = new THREE.PerspectiveCamera(fov,window.innerWidth/window.innerHeight, 1, 2000); //camera.target = new THREE.Vector3(0, 0, 0); // setting up the renderer ...

Unable to select image inside linked container

I'm attempting to display a dropdown menu when the user clicks on the gear-img div using jQuery. However, because it's wrapped inside an a tag, clicking redirects me to a URL. I also want the entire div to be clickable. Any suggestions for a solu ...

Protractor's count() function fails to execute properly when called outside of a promise loop

var alerts = element.all(by.xpath("//div[@class='notification-content']")); alerts.count().then(function (val) { console.log(val); }); let compareValue = val; Is there a way to access the 'value' outside of the promise l ...

Error: The module 'https://www.gstatic.com/firebasejs/9.6.0/firebase-app.js' is missing the required export 'default'

I'm currently in the process of setting up and testing Google authentication for a web application that I'm developing. Unfortunately, I've encountered several issues with getting the JavaScript functions to work properly, and I am uncertain ...

The reason for the undefined socket.id in the browser is due to a potential

When using console.log(socket), I am able to see a socket object in Firebug. Within this object, there is a property called id and I can view the value of this id. However, when I try to access it directly with console.log(socket.id), the result is undefin ...

Extracting Values from a jQuery Array Object

Good day everyone! Here is the code I am currently working with: var items = []; $(xml).find("Placemark").each(function () { var tmp_latLng = $(this).find("coordinates").text(); tmp_latLng = tmp_latLng.split(","); items.push({ name: ...

The Angular UI tree is malfunctioning on Mozilla Firefox

Check out this Plunker example. While this Plunker works well in Chrome and IE, it encounters issues in Mozilla Firefox. There seems to be a problem with the dropdown selection causing the page to reload. Any ideas on how to fix this? <script type= ...

Having trouble uploading a file in PDF format (*.pdf)

I'm attempting to use Node's readFile method to read a file and then send it as a response so that the user can download it. This is the code snippet I have: async function(req, res, next) { const query = { id: req.params.id }; // @ts-ignore co ...

Fade in background color with jquery when scrolling

Is there a way to make the header background fade in after scrolling a certain number of pixels? The code I have sort of works, but not quite right. Any suggestions? Thank you! $(function () { $(window).scroll(function () { $(document).scrol ...

Is there a way to make AJAX post on another page?

I am facing an issue where the data I send is supposed to be echoed out on the second page, but instead it echoes out on the first page. How can I ensure that it displays on the second page and not the first? Even though the code on the second page is exe ...

``Incorporating Vue.js: A Guide to Emphasizing the Chosen Selection from Numerous Lists

I am currently utilizing Vue.js and have multiple lists displayed, but I only wish to select and highlight one element at a time. Currently, every click results in multiple items being highlighted. I hope that explanation is clear. Below are the snippets o ...

Unable to view the refreshed DOM within the specifications after it has been altered

For my current project, I am working on writing a functional spec that involves using Mocha/JSDOM and making assertions with 'chai'. The specific use case I am tackling is related to the function called updateContent: When this function is exec ...

Why does the 401 error continue to persist while attempting to log in using Google Identity service on my Laravel application?

Trying to implement Google authentication services for user authentication. I've already integrated Laravel sanctum to allow users to log in and register successfully. This time, I want to add Google Identity services as an additional authentication ...

Having difficulty transferring data from a JSON file on a different domain to a variable using Ajax

As a beginner in Ajax, I am currently exploring the use of Ajax Cross domain functionality to fetch data. The Ajax function is triggered from test.php, which then calls stats.php to request the desired data. The content of Stats.php: <?php $data = ...

Tips for Maintaining User Data Across Pages in React using React-Router-Dom and Context

I've been tackling the login functionality of a client-side application. Utilizing React alongside TypeScript, I've incorporated react-router-dom and Context to manage the user's data when they log in. However, upon refreshing the page, the ...

Create individual account pages with specific URLs in Next.js

I'm currently working on developing a website that will feature individual user pages showcasing their posts and additional information. I'm facing some difficulty in figuring out how to generate new links to access these user accounts. For insta ...