1

I've tried every suggested solution and looked at every similar question, but nothing is resolving my issue. This script works perfectly fine on my local machine. The issue with Lambda is that it completely skips time out's and awaits. Whereas my local machine actually waits.

My index file looks like this:

import { getData } from './src/EC2Scripts/taskScripts/otherLogic.js';

async function handler(event) {
        await getData();
        
        const response = {
            statusCode: 200,
            body: JSON.stringify("Not waiting."),
        };

        return response;
}

export { handler };

Here's the otherLogic class:

import fetch from "node-fetch";
import { PoolConnection } from '../common/classes/PoolConnection.js';

async function getData() {
    return new Promise(async (resolve, reject) => {
       let pool = PoolConnection.getInstance();
       let currentResponse = await fetch(
            "dummyURL"
       );
        
       let currentData = await currentResponse.json();
        
       pool.uploadCoins(currentData).then((result) => { 
            pool.endConnection().then((end) => { resolve() }); 
       });
    });
}

export { getData };

Here's the PoolConnection class:

import pg from "pg";
import fetch from "node-fetch";

import { getCurrentDateTime } from "../dateTime.js";

class PoolConnection {
    // DO NOT CALL THIS CONSTRUCTOR, CALL THE GETINSTANCE() METHOD
    constructor() {
        if (!PoolConnection.instance) {
            PoolConnection.instance = this;

            this.config = {
                user: "user",
                password: "password",
                database: "postgres",
                host: "host",
                port: 5432,
                max: 100,
                connectionTimeoutMillis: 30000,
                idleTimeoutMillis: 30000
            };
            this.pool = this.establishConnection(1).catch((error) => { });
        }
    }

    static getInstance() {
        if (PoolConnection.instance) {
            return PoolConnection.instance;
        }

        PoolConnection.instance = new PoolConnection();
        return PoolConnection.instance;
    }

    async connect() {
        return new Promise((resolve, reject) => {
            const poolPromise = new pg.Pool(this.config);
            poolPromise.connect()
                .then(pool => {
                    console.log(getCurrentDateTime() + " ----- Connection to article database successful.");
                    resolve(pool);
                })
                .catch(err => {
                    console.error(getCurrentDateTime() + " ----- ERROR CONNECTING TO ARTICLE DATABASE :: " + err);
                    reject(err);
                });
        });
    }

    async establishConnection(attempts) {
        return new Promise((resolve, reject) => {
            if (attempts > 5) {
                console.log(getCurrentDateTime() + " ---- Connection unsuccessful to database, maximum number of attempts reached.");
                reject("ERROR");
            }

            this.connect().then(pool => {
                resolve(pool);
            }).catch(err => {
                console.error(getCurrentDateTime() + " RETRYING CONNECTION TO DATABASE ATTEMPT #" + attempts);
                attempts++;
                // try again in 3 seconds
                setTimeout(() => { this.establishConnection(attempts) }, 3000);
            });
        })
    }

    //  Connection has to be terminated gracefully or else script will hang.
    async endConnection() {
        return new Promise((resolve, reject) => {
           this.pool.then(connection => connection.end(() => console.log(getCurrentDateTime() + " ---- Connection to database successfully terminated."))); 
        });
    }

    async uploadData(data) {
        return new Promise((resolve, reject) => {
           this.pool.then(async (poolInstance) => {
                for(const entry of data) {
                    
                    let getMoreData = await fetch (
                        "dummyUrl2" + entry
                    );
    
                    const result = poolInstance.query("INSERT INTO table(data) VALUES ($1)", 
                                                    [entry['getMoreData']]);
                }
                resolve();
            }); 
        });
    }
}

export { PoolConnection }

It tries to close the connection before I can do anything. Here's the error:

> START RequestId: 5aa4984b-d57f-4d69-84a3-005a6c590c0f Version: $LATEST
> 2022-05-03T14:48:11.625Z  5aa4984b-d57f-4d69-84a3-005a6c590c0f    INFO    2022-5-3
> **** 14:48:11 ----- Connection to article database successful. 2022-05-03T14:48:12.946Z   5aa4984b-d57f-4d69-84a3-005a6c590c0f    INFO    2022-5-3
> **** 14:48:12 ---- Connection to database successfully terminated. 2022-05-03T14:48:14.146Z   5aa4984b-d57f-4d69-84a3-005a6c590c0f    ERROR   Unhandled Promise Rejection
>   {"errorType":"Runtime.UnhandledPromiseRejection","errorMessage":"Error:
> Client was closed and is not
> queryable","reason":{"errorType":"Error","errorMessage":"Client was
> closed and is not queryable","stack":["Error: Client was closed and is
> not queryable","    at
> /var/task/node_modules/pg/lib/client.js:570:27","    at
> processTicksAndRejections
> (internal/process/task_queues.js:77:11)"]},"promise":{},"stack":["Runtime.UnhandledPromiseRejection:
> Error: Client was closed and is not queryable","    at
> process.<anonymous> (/var/runtime/index.js:35:15)","    at
> process.emit (events.js:400:28)","    at processPromiseRejections
> (internal/process/promises.js:245:33)","    at
> processTicksAndRejections (internal/process/task_queues.js:96:32)"]}
> END RequestId: 5aa4984b-d57f-4d69-84a3-005a6c590c0f REPORT RequestId:
> 5aa4984b-d57f-4d69-84a3-005a6c590c0f  Duration: 2985.31 ms    Billed
> Duration: 2986 ms Memory Size: 128 MB Max Memory Used: 75 MB  Init
> Duration: 267.75 ms

I've tried converting everything to return a new Promise and then using .then(), but that actually made the program run even faster somehow. I've tried using Promise.all().then(). I've tried moving the setTimeout() into the handler in the index.js, but that didn't work either. I've tried using then() after the pool.uploadData(). I've also tried removing async from the method handler.

I'm out of ideas at this point. I've refactored and tried to implement this code in every way I've seen, and nothing is working.

There are no errors or uncaught exceptions, it's just not waiting for anything.

Lambda is using NodeJS version 14.x

EDIT: added pool connection class and updated classes to show more clearly what I have tried.

Mike Ehnes
  • 21
  • 5
  • You cannot just throw in random pieces of code and hope for the best. And the approach with the timeout is rather clumsy, because if you get it to work, you will always be billed at least for 5 second. What is `uploadData()`. You should make this one returning a promise that resolves, once the data is written and remove the timeout – derpirscher May 03 '22 at 07:09
  • It wasn't random, I was saying that I modified the code to try and implement it in every way that looked to be a solution to my issue. I've tried making it return a promise. – Mike Ehnes May 03 '22 at 14:22
  • `poolInstance.query` most certainly is async as well, but you are not awaiting it's result. Thus you are just rushing throgh the loop, starting a few async processes and then call resolve ... – derpirscher May 03 '22 at 15:15
  • @derpirscher What's the proper way to await a for loop? I've done logging it if the queries can execute fast enough, they finish before the script closes and it looks like a success, but if they don't it fails. I put a log right before the resolve and it's not waiting for the loop to finish even though I put await infront of the query. It looks to be the proper way according to docs I've found, but it's not acting the way it appears it should. – Mike Ehnes May 04 '22 at 02:24
  • You don't have to await the loop, you have to await the query. Look at the docs of the library you are using tif `query` returns a promise. If it does use `await pool.query(...)` If it uses a callback, wrap it into a promise. – derpirscher May 04 '22 at 06:04
  • And you should not mix `async/await` with `.then().catch()`. That's bad style and will lead to problems. There are literally thousands of tutorials about async programming out there. Grab some of them and read them thoroughly. Judging from your code and questions you seem to have some basic misunderstandings about the basic concepts – derpirscher May 04 '22 at 06:08
  • I figured out how to await the loop. I did have to wait on it since Node would resolve before the last iteration of the loop and consistently close the connection before the last thing could be queried. Maybe it's a version mismatch, but I went through a minimum of 20 guides on how to properly await loops and none of them worked, all gave the same result. I will update this post with what worked for me. – Mike Ehnes May 04 '22 at 17:57

2 Answers2

2

While getData() is marked as async, it doesn't return any explicit promises so it returns a Promise<void> which immediately resolves (to undefined).

You need to return an explicit Promise which resolves/rejects after running your business logic to make Lambda actually wait for processing to complete.

As per Node.js Lambda docs on async handlers:

If your code performs an asynchronous task, return a promise to make sure that it finishes running. When you resolve or reject the promise, Lambda sends the response or error to the invoker.

For example, this returns a Promise which resolves after 5 seconds, making Lambda wait before it returns a response:

async function getData() {
    return new Promise((resolve, reject) => {
        setTimeout(() => resolve(), 5000)
    });
}

async function handler(event) {
    await getData();

    const response = {
        statusCode: 200,
        body: JSON.stringify("Waiting 5 seconds..."),
    };

    return response;
}

export { handler };
START RequestId: 0ee7693e-8d6a-4c3f-b23e-119377e633e3 Version: $LATEST
END RequestId: 0ee7693e-8d6a-4c3f-b23e-119377e633e3
REPORT RequestId: 0ee7693e-8d6a-4c3f-b23e-119377e633e3  Duration: 5009.05 ms    Billed Duration: 5010 ms    Memory Size: 128 MB Max Memory Used: 56 MB  Init Duration: 224.62 ms
Ermiya Eskandary
  • 15,323
  • 3
  • 31
  • 44
  • 1
    actually, as `GetData` is `async` it returns a promise per definition. But it's a `Promise` that immediately resolves. – derpirscher May 03 '22 at 06:39
  • Of course, the proper approach to resolve this issue would be to make `pool.uploadData` awaitable and just do `await pool.uploadData(currentData);` instead of waiting for a random amount of time. Because most of the time, this timeout will be far too long and OP will be billed for useless waiting, but on the other hand in some rare cases, it might be too short and the upload request won't finish – derpirscher May 03 '22 at 12:13
  • @derpirscher yes of course, I don’t know the business logic so I’m just demonstrating some code that actually works in terms of asynchronous handlers :) – Ermiya Eskandary May 03 '22 at 14:24
  • @ErmiyaEskandary I updated the question to better explain the method that I tried initially. I tried to return promises, but it doesn't work for me. The only thing I can think of that is throwing this off is that in the uploadData() method, I'm using async in the .then(). – Mike Ehnes May 03 '22 at 15:08
  • @MikeEhnes don’t mix await and .then - use await all the way – Ermiya Eskandary May 03 '22 at 22:14
  • @ErmiyaEskandary I've looked at the docs and when I use await before the query, it seems to not care and still resolve before the queries are done. I've console logged that this is the case. As well as the connection never being properly disconnected because the script exits before then. – Mike Ehnes May 04 '22 at 02:28
0

I'm adding this because I was able to figure out what was going wrong. With some help from previous answers/comments, I've reformatted my code so if anyone else runs into a similar issue, hopefully, this helps.

index.js:

import { getData } from './src/EC2Scripts/taskScripts/otherLogic.js';

async function handler(event) {
        await getData();
        
        const response = {
            statusCode: 200,
            body: JSON.stringify("Now waiting."),
        };

        return response;
}

export { handler };

other logic class:

import fetch from "node-fetch";
import { PoolConnection } from '../common/classes/PoolConnection.js';

async function getData() {
    return new Promise(async (resolve, reject) => {
       let pool = await PoolConnection.getInstance();
       let currentResponse = await fetch(
            "dummyURL"
       );
        
       let currentData = await currentResponse.json();
       await pool.uploadData(currentData);
       await pool.endConnection();
       resolve();
    });
}

export { getData };

PoolConnection class:

import pg from "pg";
import fetch from "node-fetch";

import { getCurrentDateTime } from "../dateTime.js";

class PoolConnection {
    // DO NOT CALL THIS CONSTRUCTOR, CALL THE GETINSTANCE() METHOD
    constructor() {
        if (!PoolConnection.instance) {
            PoolConnection.instance = this;

            this.config = {
                user: "user",
                password: "password",
                database: "postgres",
                host: "host",
                port: 5432,
                max: 100,
                connectionTimeoutMillis: 30000,
                idleTimeoutMillis: 30000
            };

            this.pool = new pg.Pool(this.config);
        }
    }

    static async getInstance() {
        return new Promise(async (resolve, reject) => {
            if (PoolConnection.instance) {
                return PoolConnection.instance;
            }

            PoolConnection.instance = new PoolConnection();
            resolve(PoolConnection.instance);
        });
    }

    //  Connection has to be terminated gracefully or else script will hang.
    async endConnection() {
        return new Promise(async (resolve, reject) => {
            await this.pool.end(() => console.log(getCurrentDateTime() + " ---- Connection to database successfully terminated."));
            resolve();
        });
    }

    async uploadData(data) {
        return new Promise(async (resolve) => {
            let promiseArray = [];

            for (const entry of data) {
                promiseArray.push(new Promise(async (resolve) => {
                    await this.pool.connect(async (error, client, release) => {
                        if (error) {
                            console.error(getCurrentDateTime() + " ----- Error getting client. Error :: ", error.stack);
                        }
                        await client.query("query", (error, result, release) => {
                                release();
                                if (error) {
                                    console.error(getCurrentDateTime() + " ----- Error executing query :: ", error.stack);
                                }
                                resolve();
                         });
                    });
                }));
            }
            await Promise.all(promiseArray);
            resolve();
        });
    }
}

export { PoolConnection }

If this doesn't solve your issue, the two biggest helps for finding a fix were this post and this post.

Mike Ehnes
  • 21
  • 5