1

I have an interesting problem with Sequelize, the Node ORM library. I wish to insert this data into an empty table:

[
      { title: 'Expenditure 1', amount: 100 },
      { title: 'Expenditure 2', amount: 200 },
      { title: 'Expenditure 3', amount: 300 },
      { title: 'Expenditure 4', amount: 400 },
      { title: 'Expenditure 5', amount: 500 },
      { title: 'Expenditure 6', amount: 600 },
      { title: 'Expenditure 7', amount: 700 },
      { title: 'Expenditure 8', amount: 800 },
      { title: 'Expenditure 9', amount: 900 },
      { title: 'Expenditure 10', amount: 1000 },
      { title: 'Expenditure 11', amount: 1100 },
      { title: 'Expenditure 12', amount: 1200 },
      { title: 'Expenditure 13', amount: 1300 },
      { title: 'Expenditure 14', amount: 1400 },
      { title: 'Expenditure 15', amount: 1500 },
      { title: 'Expenditure 16', amount: 1600 },
      { title: 'Expenditure 17', amount: 1700 },
      { title: 'Expenditure 18', amount: 1800 },
      { title: 'Expenditure 19', amount: 1900 },
      { title: 'Expenditure 20', amount: 2000 }
]

As one would expect, in a fresh table, these will be given id values of 1, 2, 3, etc in ascending order. So my first attempt to write these is thus:

  const writeToTable = (model, rows) => {
    let promises = rows.map((row) => model.create(row));
    return Promise.all(promises);
  }

OK, it sort of works, but the rows get inserted in an unpredictable order, since Promise.all runs the promises in parallel, or at least does not guarantee to run them in order.

So I next tried this (using some code from this answer):

  const writeToTable = (model, rows) => {
    let promises = rows.map((row) => model.create(row));
    let chain;

    for (let i in promises) {
       if (chain) chain = chain.then(() => promises[i]);
       if (!chain) chain = promises[0];
    }

    return chain;
  }

That looks like it must run the items in order, but no - running the code several times can produce different results. Odd!

So I next try this:

  const writeToTable = async (model, rows) => {
    let promises = rows.map((row) => model.create(row));

    for (let promise of promises) {
       await promise;
    }
  };

This is async, and has no return value, so should automatically return a Promise that resolves once all internal awaited calls are resolved. But, no - I still get occasionally wrongly ordered rows.

What technique could I use to ensure that my calls are executed in order? I suspect that Sequelize is doing some cacheing/late-writing here, and getting itself into a pickle. I am happy to hear of approaches in Sequelize that hand off a whole array to be inserted, but that feels a bit like cheating - I wonder if I would like to understand why this is failing.

I am absolutely sure the source array is correct.

It may help to know that these calls are being made in Jest to set up test data before calling a service and running assertions.

halfer
  • 19,824
  • 17
  • 99
  • 186
  • Have you tried recursion? It sounds like maybe recursion may work better here, although it'll be less efficient, you can add the index as an argument, keeping order from 0 ... N, checking to ensure index < array.length, using await insertedRow, before proceeding to the next method? Would that work in your case? As a side note, you could just keep `.shift()` items until there's nothing left to add, again keeping the order they're presented in. – Isolated May 09 '20 at 16:37
  • @Isolated: thanks for the idea, but it sounds a bit convoluted just to inject a bunch of rows. I am not sure I would have any confidence that the promises would then resolve in the correct order anyway - it feels like Sequelize is monkeying around here. – halfer May 09 '20 at 16:39

1 Answers1

1

Once a Promise has been created, actions inside it will run to the end no matter how the Promise's resolution is waited for. For example:

const prom1 = makeProm1();
const prom2 = makeProm2();

At this point, the Promises have started, and will do whatever they're supposed to do (in parallel) even if you later await them separately:

await prom1;
await prom2;

const makeProm = num => new Promise(
  resolve => setTimeout(
    () => {
      console.log(num + ' resolving');
      resolve();
    },
    Math.random() * 1000
  )
);

(async () => {
  const prom1 = makeProm(1);
  const prom2 = makeProm(2);
  await prom1;
  await prom2;
  console.log('Done');
})();

That's what each of your snippets are doing - the .map creates a new Promise immediately for every item of the rows array, so whatever you do afterwards won't affect the order in which those Promises resolve.

You need to change things so that you only create a Promise when you're ready for it to start its action - that is, only after the previous Promise has finished executing:

const writeToTable = (model, rows) => {
  for (const row of rows) {
    await model.create(row);
  }
}
halfer
  • 19,824
  • 17
  • 99
  • 186
CertainPerformance
  • 356,069
  • 52
  • 309
  • 320
  • Ooh! I suspected on my last couple of questions that there was something about promises that I was not grokking, and this rather confirms it. Thanks CertainPerformance! I will make some experiments and adjustments based on this information. – halfer May 09 '20 at 16:41
  • Out of interest, do Promises have "start" logic in their constructor? I am rather new to Node, and am coming from the PHP world, where it is thought that doing anything more than set-up in a ctor is bad practice, and makes for difficult testing. – halfer May 09 '20 at 16:42
  • When you use the Promise *constructor* (`new Promise`), it will only ever do exactly what you tell it to do. You might put logic in it (I'd hope so, otherwise the Promise would probably be useless), or you might not. When you interface with something that *returns* a Promise (like `model.create`, or `fetch`, or another API), it will almost always have initialized the asynchronous action when the Promise is returned, and the asynchronous action will (almost always) then run to the end, no matter how the Promise is used. – CertainPerformance May 09 '20 at 16:48
  • In other words, you pretty much never see something like this: `const somePromise = api.makeProm(); api.runProm();` That'd be really weird – CertainPerformance May 09 '20 at 16:49
  • Righto, yes - I was aware that `model.create()` was going to return a Promise, but perhaps I was thinking they operate a bit like like threads, which need to be explicitly started. I think I have several wrong learnings to unpick, but that is very useful, thanks. – halfer May 09 '20 at 16:50
  • 1
    Yeah, the starting of the action is usually synonymous with the returning of a Promise to the consumer. Set-up is still doable, though. You might see `someApi.setConfig(configObj); const results = await someApi.startAction();` – CertainPerformance May 09 '20 at 16:53