Execução imprevisível de código assíncrono em Lambda node.js

Nov 05 2020

Não consigo entender como usar o código assíncrono no Lambda. Os resultados são confusos, vamos construir 2 funções:

const sendToFirehoseAsync = async (param) => {
    console.log(param);
    const promise = new Promise(function(resolve, reject) {

        var params = {
            DeliveryStreamName: 'TestStream', 
            Records: [{ Data: 'test data' }]
        };
        console.log('params', params);

        firehose.putRecordBatch(params, function (err, data) {
            if (err) console.log(err, err.stack); // an error occurred
            else console.log('Firehose Successful',  data);           //         successful response
        });
    });
    return promise;
}

e

const sendToFirehoseSync = (param) => {
    console.log(param);

    var params = {
        DeliveryStreamName: 'TestStream', 
        Records: [{ Data: 'test data' }]
    };
    console.log('params', params);

    firehose.putRecordBatch(params, function (err, data) {
        if (err) console.log(err, err.stack); // an error occurred
        else console.log('Firehose Successful',  data);           //         successful response
    });  
}

Agora vamos executá-los e ver o que acontece!

Execute a função Async - Funciona bem.

exports.handler = async (event) => {
    let res = await sendToFirehoseAsync('test1');
    return res;
}

    2020-11-05T20:19:16.146+13:00   START RequestId: e4c505ea-1717-4998-ad0d-a42434f0a0c1 Version: $LATEST
    2020-11-05T20:19:16.148+13:00   2020-11-05T07:19:16.147Z e4c505ea-1717-4998-ad0d-a42434f0a0c1 INFO test1
    2020-11-05T20:19:16.149+13:00   2020-11-05T07:19:16.149Z e4c505ea-1717-4998-ad0d-a42434f0a0c1 INFO params { DeliveryStreamName: 'TestStream', Records: [ { Data: 'test data' } ] }
    2020-11-05T07:19:16.245Z    e4c505ea-1717-4998-ad0d-a42434f0a0c1    INFO    Firehose Successful {
        FailedPutCount: 0,
        Encrypted: false,
        ....

No entanto, se eu chamar a função duas vezes com await (veja abaixo), obtenho exatamente a mesma resposta (ou seja, não vejo o console.log do teste 2, etc. É como se a segunda chamada nunca acontecesse? O que está acontecendo? Presumi que awaitiria interromper a execução até que a primeira função fosse resolvida e, em seguida, continuar.Claro que não.

let res = await sendToFirehoseAsync('test1');
res = await sendToFirehoseAsync('test2');
return res;

Agora vamos executar mais alguns consecutivos:

console.log('async call 1');
await sendToFirehoseAsync('test1');

console.log('async call 2');
await sendToFirehoseAsync('test2');

console.log('sync call 1');
let resp1 = await sendToFirehoseSync('1');

console.log('sync call 2');
let resp2 = await sendToFirehoseSync('2');

console.log('after sync calls');

2020-11-05T20:35:28.465+13:00   2020-11-05T07:35:28.464Z 5a9e551f-ecc6-4f18-8af4-a11b1b29d835 INFO async call 1
2020-11-05T20:35:28.465+13:00   2020-11-05T07:35:28.465Z 5a9e551f-ecc6-4f18-8af4-a11b1b29d835 INFO test1
2020-11-05T20:35:28.467+13:00   2020-11-05T07:35:28.467Z 5a9e551f-ecc6-4f18-8af4-a11b1b29d835 INFO params { DeliveryStreamName: 'TestStream', Records: [ { Data: 'test data' } ] }
2020-11-05T20:35:28.577+13:00   2020-11-05T07:35:28.577Z 5a9e551f-ecc6-4f18-8af4-a11b1b29d835 INFO Firehose Successful { FailedPutCount: 0, Encrypted: false, RequestResponses: [ { RecordId: '4v+6H3T3koBggYYvdu/U6fg4h0C8m4taPVYznfYT4fIAWmm9XKu4/9F9jEgjdZFE02IsNgYs0/ORGzz1l2udEzCJUN1dRR1YCHSi/jiLI/DHGpTkoyN89VUG0jGzNlAERgUNCIwxXlCYww/l2HSGjK8++f+qmRj7sTCY/J4/QlV2sqhcXSlJjKhkK+A+Ib7w2+WwdZ5gliF64fSP9qkQSpeSutOh68o6' } ] }
2020-11-05T20:35:28.580+13:00   END RequestId: 5a9e551f-ecc6-4f18-8af4-a11b1b29d835

Novamente, apenas 1 resultado retorna. O resto está perdido ?!

E obtenho o mesmo resultado com 2 chamadas assíncronas, seguidas por 2 chamadas de sincronização e mais uma para garantir.

console.log('async call 1');
await sendToFirehoseAsync('test1');

console.log('async call 2');
await sendToFirehoseAsync('test2');

console.log('sync call 1');
let resp1 = await sendToFirehoseSync('1');

console.log('sync call 2');
let resp2 = await sendToFirehoseSync('2');

console.log('after sync calls');


const promise = new Promise(function(resolve, reject) {

    var params = {
        DeliveryStreamName: 'TestStream', 
        Records: [{ Data: 'test data 2' }]
    };
    console.log('params', params);

    firehose.putRecordBatch(params, function (err, data) {
        if (err) console.log(err, err.stack); // an error occurred
        else console.log('Firehose Successful',  data);           //         successful response
    });
});

return promise;  

No entanto ... Se eu executar novamente o último exemplo, mas comentar as 2 chamadas assíncronas, obtenho algo diferente ...

console.log('sync call 1');
let resp1 = await sendToFirehoseSync('1');

console.log('sync call 2');
let resp2 = await sendToFirehoseSync('2');

console.log('after sync calls');


const promise = new Promise(function(resolve, reject) {

    var params = {
        DeliveryStreamName: 'TestStream', 
        Records: [{ Data: 'test data 2' }]
    };
    console.log('params', params);

    firehose.putRecordBatch(params, function (err, data) {
        if (err) console.log(err, err.stack); // an error occurred
        else console.log('Firehose Successful',  data);           //         successful response
    });
});

return promise;

2020-11-05T20:42:08.713+13:00
2020-11-05T07:42:08.713Z    333feae9-f306-409c-89c8-1707e0547ba3    INFO    sync call 1
2020-11-05T07:42:08.713Z 333feae9-f306-409c-89c8-1707e0547ba3 INFO sync call 1
2020-11-05T20:42:08.713+13:00   2020-11-05T07:42:08.713Z 333feae9-f306-409c-89c8-1707e0547ba3 INFO 1
2020-11-05T20:42:08.715+13:00   2020-11-05T07:42:08.715Z 333feae9-f306-409c-89c8-1707e0547ba3 INFO params { DeliveryStreamName: 'TestStream', Records: [ { Data: 'test data' } ] }
2020-11-05T20:42:08.760+13:00   2020-11-05T07:42:08.760Z 333feae9-f306-409c-89c8-1707e0547ba3 INFO after sync calls
2020-11-05T20:42:08.760+13:00   2020-11-05T07:42:08.760Z 333feae9-f306-409c-89c8-1707e0547ba3 INFO params { DeliveryStreamName: 'TestStream', Records: [ { Data: 'test data 2' } ] }
2020-11-05T20:42:08.808+13:00   2020-11-05T07:42:08.807Z 333feae9-f306-409c-89c8-1707e0547ba3 INFO Firehose Successful { FailedPutCount: 0, Encrypted: false, RequestResponses: [ { RecordId: 'iWeCDK6kukfkLfh/1mg791g3sIVpDC1hNNokJuTGFJJaLBNd1TvvCiWHV4z2iiWS3hOvu9OmKVnUofCPbr5uewKPAQBdiCJp9iVIzTakcL5bb4CkyOZKxzLX4NOxTP94Z0j64KgssWo10z7jEhDoevF8NTMZR+tUlhHmYtEGcQq2YViwwXhpYX8MP4yvS5xSRo+sjJXEcyoty+Pvt1UFWGelEKIygtnO' } ] }
2020-11-05T20:42:08.865+13:00   2020-11-05T07:42:08.865Z 333feae9-f306-409c-89c8-1707e0547ba3 INFO Firehose Successful { FailedPutCount: 0, Encrypted: false, RequestResponses: [ { RecordId: 's5loZTT8d4J0fhSjnJli0LzOzljnvgvC99AvdSeqkj/j9xp5RnjstL5UxQXm5t+uyEbSSe21XZxwaUU/D7XVsCzpJ6F5nlnzsOZBLd6vyaF3bc2lSUo2DM2u9dGetJPMahC1b0rO+GXod91sC9XumS8QWIVePcww2DH0IM46RuoLEVVR3/kgcnvhIm/UU67JuvZkFTCAP/jss0VwVUY2vmzfdvw4mJT4' } ] }
2020-11-05T20:42:08.867+13:00   END RequestId: 333feae9-f306-409c-89c8-1707e0547ba3

O único padrão que consigo ver é o retorno. A função assíncrona tem um retorno. Isso talvez faça com que o lambda inteiro retorne, não apenas a função? Espero que esta longa experiência (infelizmente) seja útil e que alguém possa lançar alguma luz sobre como isso funciona. Felicidades.

** Adicionando resolução e rejeição **

console.log('before');
await sendToFirehosePromise('thing');
console.log('after');

....

async function sendToFirehosePromise(record) {
    var params = {
        DeliveryStreamName: 'TestStream', 
        Records: [{ Data: 'test data' }]
    };
    
    const promise =  new Promise((resolve, reject) => {
        firehose.putRecordBatch(params, (err, data) => {
            if (err) return reject(err);
            return resolve(data);
        });
    });       
    return promise;
}

Respostas

1 404 Nov 05 2020 at 10:51

Vamos reduzir o máximo possível para chamadas assíncronas puras de um lambda:

function f(p) {
    console.log(p);
    return new Promise(res => res('result ' + p));
}

exports.handler = async () => {
    let res = await f(1);
    console.log(res);
    res = await f(2);
    console.log(res);
    res = await f(3);
    console.log(res);
}

Impressões:

2020-11-05T10:40:25.298Z    bd564d91-480f-4dcd-8134-1481fa59a946    INFO    1
2020-11-05T10:40:25.298Z    bd564d91-480f-4dcd-8134-1481fa59a946    INFO    result 1
2020-11-05T10:40:25.298Z    bd564d91-480f-4dcd-8134-1481fa59a946    INFO    2
2020-11-05T10:40:25.298Z    bd564d91-480f-4dcd-8134-1481fa59a946    INFO    result 2
2020-11-05T10:40:25.298Z    bd564d91-480f-4dcd-8134-1481fa59a946    INFO    3
2020-11-05T10:40:25.299Z    bd564d91-480f-4dcd-8134-1481fa59a946    INFO    result 3

Agora compare com o seu. Em primeiro lugar esta assinatura está errado (ou pelo menos desnecessário): const sendToFirehoseAsync = async (param). Async só é necessário se você estiver aguardando algo. Se você não está esperando - e não está - não precisa marcá-lo como assíncrono. Uma função assíncrona pode aguardar qualquer coisa que retorne uma promessa. Se sua função retorna uma promessa e não espera nada, não a marque como assíncrona.

Agora vamos para a parte em que você mistura promessas e assíncrona desnecessariamente.

async function sendToFirehosePromise(record) {
    var params = {
        DeliveryStreamName: 'TestStream', 
        Records: [{ Data: 'test data' }]
    };
    
    const promise =  new Promise((resolve, reject) => {
        firehose.putRecordBatch(params, (err, data) => {
            if (err) return reject(err);
            return resolve(data);
        });
    });       
    return promise;
}

Se você está tentando usar async, use async. Todas as chamadas do SDK da AWS retornam um tipo de AWS.Request. Esse tipo contém um promise()método. Você pode aguardar essa promessa em vez de usar a notação de promessa real.

async function sendToFirehosePromise(record) {
    var params = {
        DeliveryStreamName: 'TestStream', 
        Records: [{ Data: 'test data' }]
    };
    return await firehose.putRecordBatch(params).promise();
}

Esse é um uso adequado de async / await com o SDK, em vez de colocá-lo em promessas personalizadas. Agora, em seu manipulador, você pode aguardar essa função quantas vezes quiser e ela sempre funcionará.