0

I'm trying to fetch some data from redis, do something with it, then store it back. So I have two calls to redis:

_.each(guids, async (guid) => {
            const targetRecordSl = await this.redisConn.ft_searchAsync(this.TargetIndex, [`@guid:{${guid}}`, 'RETURN', 1, 'sl']);
            console.log('Trying to add sl:' + slId + ' to: '+guid+ ' existing value:'+ targetRecordSl[2][1]);
            let sl = '';
            if(targetRecordSl[2][1] != '_null' && !targetRecordSl[2][1].includes(slId)) {
                sl = targetRecordSl[2][1] + ', ' + slId;
            } else {
                sl = slId;
            }
            const response = await this.redisConn.ft_addAsync(this.TargetIndex, [`${this.TargetIndex}:${guid}`, 1, 'REPLACE', 'PARTIAL', 'FIELDS', 'sl', sl]);
            console.log(response);

the first query executes all iterations of the loop without waiting for the second query to finish:

Trying to add sl:11 to: P6d43d914bea8b91ece2a0a3c081b9b85 existing value:10
Trying to add sl:10 to: P6d43d914bea8b91ece2a0a3c081b9b85 existing value:10
Trying to add sl:9 to: P24e58b30a0658b8f0e5f9ce1ca0acc1f existing value:9
Trying to add sl:8 to: P7345d54686bfd491747eefd4e05d0362 existing value:8
OK
OK
OK
OK

This is not what I want. I want one promise to wait for the other to finish:

Trying to add sl:11 to: P6d43d914bea8b91ece2a0a3c081b9b85 existing value:10
OK
Trying to add sl:10 to: P6d43d914bea8b91ece2a0a3c081b9b85 existing value:10
OK
Trying to add sl:9 to: P24e58b30a0658b8f0e5f9ce1ca0acc1f existing value:9
OK
Trying to add sl:8 to: P7345d54686bfd491747eefd4e05d0362 existing value:8
OK

Advices please.

ape_face
  • 101
  • 1
  • 7
  • In the callback for each promise, do an infinite while loop and only proceed if the other callback is also called, you can break out of the while loop when a global variable suggests two callback are in queue and are waiting. – Ali Ahmadi Sep 07 '19 at 12:30
  • 7
    @AliAhmadi using a while loop to wait for the other callback is already a bad idea, and using a global makes it even worse. – t.niese Sep 07 '19 at 12:33
  • `_.each`, like `forEach`, doesn't care if you pass an async and doesn't await it. – georg Sep 07 '19 at 14:17

2 Answers2

2

Maybe you can try to do it with a normal for loop:

for (const guid of guids) {
  const targetRecordSl = await this.redisConn.ft_searchAsync(this.TargetIndex, [`@guid:{${guid}}`, 'RETURN', 1, 'sl']);

  console.log('Trying to add sl:' + slId + ' to: ' + guid + ' existing value:' + targetRecordSl[2][1]);

  let sl = '';
  if (targetRecordSl[2][1] != '_null' && !targetRecordSl[2][1].includes(slId)) {
    sl = targetRecordSl[2][1] + ', ' + slId;
  } else {
    sl = slId;
  }

  const response = await this.redisConn.ft_addAsync(this.TargetIndex, [`${this.TargetIndex}:${guid}`, 1, 'REPLACE', 'PARTIAL', 'FIELDS', 'sl', sl]);

  console.log(response);
}

And if this is top level code, you could try:

async function processGuids(guids) {
    for (const guid of guids) {
        const targetRecordSl = await this.redisConn.ft_searchAsync(this.TargetIndex, [`@guid:{${guid}}`, 'RETURN', 1, 'sl']);

        console.log('Trying to add sl:' + slId + ' to: ' + guid + ' existing value:' + targetRecordSl[2][1]);

        let sl = '';
        if (targetRecordSl[2][1] != '_null' && !targetRecordSl[2][1].includes(slId)) {
            sl = targetRecordSl[2][1] + ', ' + slId;
        } else {
            sl = slId;
        }

        const response = await this.redisConn.ft_addAsync(this.TargetIndex, [`${this.TargetIndex}:${guid}`, 1, 'REPLACE', 'PARTIAL', 'FIELDS', 'sl', sl]);

        console.log(response);
    }
}

processGuids(guids)
    .then(() => {
        console.log('JOB DONE!!')
        // Here goes your following code (if it has to be executed after processing all guids)
    });
t.niese
  • 39,256
  • 9
  • 74
  • 101
Marcos Luis Delgado
  • 1,289
  • 7
  • 11
  • 2
    Why do you write `Maybe you can try`? Do you have any concerns about your answer? If the code is executed within an `async` function then this is a good way to execute the code in sequence. – t.niese Sep 07 '19 at 12:36
  • it does the same – ape_face Sep 07 '19 at 12:38
  • console.log(response); logs 'OK'. i tried this already and it does the same as lodashes each – ape_face Sep 07 '19 at 12:42
  • 1
    @ape_face are you execute the code block where the `for of` function is multiple times? Because the way it is shown here in this answer there is no way that would produce first logs you showed in your question. It has to show `Trying to add sl` and `OK` alternating. – t.niese Sep 07 '19 at 12:50
  • 2
    @MarcosLuis It does not really matter which redis package it is. As there is no way that your first code block with the `for of` would result in `Trying to add... Trying to add... Trying to add... OK OK OK`, if it is only called once, the corresponding `Trying to add...` and `OK` have to follow each other no matter how `this.redisConn.ft_searchAsync` and `this.redisConn.ft_addAsync` are implemented. – t.niese Sep 07 '19 at 12:59
  • it was an .each issue in the end, the function that ran the code above was in another .each, so replacing lodash with regular for loop did the job. lesson: don't use lodash with async code – ape_face Sep 09 '19 at 13:31
0

Great question! I have done some research on how to iterate over requests asynchronously and found some articles on async iterators and generators which open up the possibility to iterate over each guid and fetch it before proceeding to the next guid.

So I've modified your code a bit. Firstly I've created a function fetchGuid that fetches a single guid.

async function fetchGuid(guid) {
    const targetRecordSl = await this.redisConn.ft_searchAsync(this.TargetIndex, [`@guid:{${guid}}`, 'RETURN', 1, 'sl']);
    console.log('Trying to add sl:' + slId + ' to: '+guid+ ' existing value:'+ targetRecordSl[2][1]);
    let sl = '';
    if(targetRecordSl[2][1] != '_null' && !targetRecordSl[2][1].includes(slId)) {
        sl = targetRecordSl[2][1] + ', ' + slId;
    } else {
        sl = slId;
    }
    const response = await this.redisConn.ft_addAsync(this.TargetIndex, [`${this.TargetIndex}:${guid}`, 1, 'REPLACE', 'PARTIAL', 'FIELDS', 'sl', sl]);
    return response.
}

This is an asynchronous generator function which will create an iterator which we can loop over. This enables us to run sequential loops like you would like. It will run until yield occurs and then waits until it get's invoked again.

async function* iterateGuids(guids) {
    for (guid of guids) {
        const response = await fetchGuid(guid);
        yield response;
    }
}

Now this third function is where you can run the code from. getSequentialGuids calls the iterateGuids function and creates an iterator. It then uses for await ... of to loop asynchronously over each of the positions in the iterator, which in this case is a promise resolving from a HTTPRequest.

async function getSequentialGuids(guids) {
    const responses = iterateGuids(guids);
    for await (const response of responses) {
        console.log(response);
    }
}

So at the end call your guids array with the getSequentialGuids function like here below and see the magic happen.

getSequentialGuids(guids);

This is also a learning experience for me with the async generators and the for await ... of syntax. So if you'll run into some problems or have more questions, please let me know so I can try help you, and myself, learn from it.

Emiel Zuurbier
  • 19,095
  • 3
  • 17
  • 32