Counter not increasing in async map function

◇◆丶佛笑我妖孽 提交于 2020-06-16 20:47:19

问题


I am working with mongodb and nodejs. I have an array of customers I have to create each inside database.

const promises2 = customers.map(async customer => {
      if (!customer.customerId) {
        const counter = await Counter.findOne({ type: "Customer" });
        console.log({counter});
        const payload = {
          customerId: counter.sequence_value,
        };
        await Customer.create(payload);
        await Counter.findOneAndUpdate({ type: "Customer" }, { $inc: { sequence_value: 1 } });
      }
    });
    await Promise.all([...promises2]);

The issue is counter is not increasing every time. I am getting same counter in all the created customers. What is the issue here?

Issue is something like this but don't have an answer.


回答1:


The problem is that all the calls overlap. Since the first thing they each do is get the current counter, they all get the same counter, then try to use it. Fundamentally, you don't want to do this:

const counter = await Counter.findOne({ type: "Customer" });
// ...
await Counter.findOneAndUpdate({ type: "Customer" }, { $inc: { sequence_value: 1 } });

...because it creates a race condition: overlapping asynchronous operations can both get the same sequence value and then both issue an update to it.

You want an atomic operation for incrementing and retrieving a new ID. I don't use MongoDB, but I think the findOneAndUpdate operation can do that for you if you add the returnNewDocument option. If so, the minimal change would be to swap over to using that:

const promises2 = customers.map(async customer => {
  if (!customer.customerId) {
    const counter = await Counter.findOneAndUpdate(
      { type: "Customer" },
      { $inc: { sequence_value: 1 } },
      { returnNewDocument: true }
    );
    console.log({counter});
    const payload = {
      customerId: counter.sequence_value,
    };
    await Customer.create(payload);
  }
});
await Promise.all([...promises2]);

...but there's no reason to create an array and then immediately copy it, just use it directly:

await Promise.all(customers.map(async customer => {
  if (!customer.customerId) {
    const counter = await Counter.findOneAndUpdate(
      { type: "Customer" },
      { $inc: { sequence_value: 1 } },
      { returnNewDocument: true }
    );
    console.log({counter});
    const payload = {
      customerId: counter.sequence_value,
    };
    await Customer.create(payload);
  }
}));

The overall operation will fail if anything fails, and only the first failure is reported back to your code (the other operations then continue and succeed or fail as the case may be). If you want to know everything that happened (which is probably useful in this case), you can use allSettled instead of all:

// Gets an array of {status, value/reason} objects
const results = await Promise.allSettled(customers.map(async customer => {
  if (!customer.customerId) {
    const counter = await Counter.findOneAndUpdate(
      { type: "Customer" },
      { $inc: { sequence_value: 1 } },
      { returnNewDocument: true }
    );
    console.log({counter});
    const payload = {
      customerId: counter.sequence_value,
    };
    await Customer.create(payload);
  }
}));
const errors = results.filter(({status}) => status === "rejected").map(({reason}) => reason);
if (errors.length) {
  // Handle/report errors here
}

Promise.allSettled is new in ES2021, but easily polyfilled if needed.

If I'm mistaken about the above use of findOneAndUpdate in some way, I'm sure MongoDB gives you a way to get those IDs without a race condition. But in the worst case, you can pre-allocate the IDs instead, something like this:

// Allocate IDs (in series)
const ids = [];
for (const customer of customers) {
  if (!customer.customerId) {
    const counter = await Counter.findOne({ type: "Customer" });
    await Counter.findOneAndUpdate({ type: "Customer" }, { $inc: { sequence_value: 1 } });
    ids.push(counter.sequence_value);
  }
}

// Create customers (in parallel)
const results = await Promise.allSettled(customers.map(async(customer, index) => {
  const customerId = ids[index];
  try {
    await Customer.create({
      customerId
    });
  } catch (e) {
    // Failed, remove the counter, but without allowing any error doing so to
    // shadow the error we're already handling
    try {
      await Counter.someDeleteMethodHere(/*...customerId...*/);
    } catch (e2) {
      // ...perhaps report `e2` here, but don't shadow `e`
    }
    throw e;
  }
});

// Get just the errors
const errors = results.filter(({status}) => status === "rejected").map(({reason}) => reason);
if (errors.length) {
  // Handle/report errors here
}



回答2:


Your map function is not returning a promise.

Try this :

const promises2 = [];

customers.map((customer) => {
  return new Promise(async (resolve) => {
    if (!customer.customerId) {
      const counter = await Counter.findOne({ type: 'Customer' });
      console.log({ counter });
      const payload = {
        customerId: counter.sequence_value,
      };
      await Customer.create(payload);
      await Counter.findOneAndUpdate({ type: 'Customer' }, { $inc: { sequence_value: 1 } });
    }
    resolve();
  });
});

await Promise.all(promises2);


来源:https://stackoverflow.com/questions/62127030/counter-not-increasing-in-async-map-function

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!