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

What steps should I take to generate a stylized date input in javascript?

Looking to dynamically create a date string in JavaScript with the following format: dd-MMM-yyyy Need the dd part to change between 1 and 29 each time I generate the variable within a loop Month (MMM) should be set as Jan ...

What is the method for defining functions that accept two different object types in Typescript?

After encountering the same issue multiple times, I've decided it's time to address it: How can functions that accept two different object types be defined in Typescript? I've referred to https://www.typescriptlang.org/docs/handbook/unions ...

Error Message: Unable to access 'map' property of undefined in TSX file

Component for displaying attendees in an activity interface IProps { attendees: IAttendee[] } export const ActivityListItemAttendees: React.FC<IProps> = ({attendees}) => { return ( <List horizontal> {attendees.ma ...

How to use getServerSideProps in Next.js

In my current scenario, I find myself within a folder in the pages directory, specifically in a file named [id].jsx. My goal is to make getServerSideProps return just the name of the page, for example /page/id123 should return id123. import Link from &a ...

Unable to change the variable for the quiz

Currently, I am in the process of developing a quiz app and I am facing an issue with my correct variable not updating. Whenever I trigger the function correctTest() by clicking on the radio button that corresponds to the correct answer, it does get execut ...

The Service Worker seems to be neglecting to serve the sub-pages at

I've successfully implemented a service worker on my website. The home page loads correctly from the Service Worker cache, and when I visit previously viewed 'posts' while online in Chrome, they load and show as 'from ServiceWorker&apos ...

How can you customize the output buffer size (known as highWaterMark) for child_process.spawn() in node.js?

I'm currently working on adjusting the buffer size (highWaterMark) for the stdout coming from a child_process.spawn() function, but I'm encountering difficulties in achieving this successfully. The code snippet provided below demonstrates my obje ...

Is there a constraint on JSON data?

Is there a limit to the amount of data that JSON with AJAX can handle in outgoing and returning parameters? I am trying to send and receive a file with 10,000 lines as a string from the server. How can I accomplish this task? Can a single parameter manage ...

Identify the currently active subitem within the item-active class in a PHP carousel slider

I am working on creating an image carousel slider with 4 items and 4 slides each. These images will act as radio buttons, and I want to highlight the slide corresponding to the selected radio button. So, when the carousel loads, the selected slide should b ...

Adjust properties based on screen size with server-side rendering compatibility

I'm currently using the alpha branch of material-ui@v5. At the moment, I have developed a custom Timeline component that functions like this: const CustomTimeline = () => { const mdDown = useMediaQuery(theme => theme.breakpoints.down("md")); ...

Using TypeScript's conditional types for assigning types in React

I'm tasked with creating a component that can belong to two different types. Let's call them Type A = { a: SomeCustomType } Type B = { b: SomeOtherDifferentType } Based on my understanding, I can define the type of this component as function C ...

Utilizing JQuery to make Google listings easily findable

Implementing Google Places for a location text box has been successful. However, I am now looking to create a class-based implementation so that I can use it with multiple places effortlessly. Is it possible to achieve this using JQuery? <script type ...

Encountered difficulties sending JSON data to a REST endpoint using Node.js

Is there a way to bulk insert JSON data into MongoDB using Mongoose? I am aware of the insertMany method, but I'm encountering difficulties with extracting the correct req.body. Below is an image of my setup in Postman. Here is my Node.js code: rout ...

Executing a component's function from a JavaScript file

Is it possible in React to call a function or method of a component from a separate JS file in order to modify the component's state? Here are three example files: First, App.js import React,{Component} from 'react'; import Login from &ap ...

Can anyone explain to me why the data I'm passing as props to the React functional component is displaying as undefined?

I have encountered an issue with a pre-made React component where I am unable to see the data being passed as props when I console log it. I am unsure if I am passing the prop correctly, as I have used the same prop successfully in other class-based comp ...

Optimizing Angular for requireJS deletion

We recently developed an Angular directive that utilizes blueimp-fileupload. Everything seems to be working fine until we decided to optimize our code using requireJs. After running the optimizer, we encountered the following error: Error: cannot call m ...

An unassigned variable automatically sets the disabled attribute to true on an input field

Is this behavior a problem or normal? Consider the following form structure: <form #form="ngForm" > <div> <label>field1</label> <input type="text" name="field1" [(ngModel)]="mainVar" [disabled]="someVar" /> ...

Mongoose: Unable to fetch item using its specific identification - Error 404

I've been attempting to fetch objects from MongoDB using Mongoose, but I keep encountering a 404 error. router.get('/blogs/:id', function(req, res){ console.log('trying to get one blog post by id'); Blog.findOne({ _id: req.para ...

Generate an li element that is interactive, containing both text and a span element

I am dealing with a JSON array that looks like this: var teamDetails=[ { "pType" : "Search Engines", "count" : 5}, { "pType" : "Content Server", "count" : 1}, { "pType" : "Search Engines", "count" : 1}, { "pType" : "Business", "count" : 1,}, { "pTyp ...

Creating a timer implementation in Node.js using Socket.IO

In the process of developing a drawing and guessing game using node.js and socket.io, I have a structure consisting of a Room class, a Game class (which is an extension of Room), and a Round class where each game consists of 10 rounds. During each round, a ...