I'm fetching data from a website but the process sometimes fails so I make the function to retry it. But I'm using a 15 seconds timeout, so when the timeout is triggered it will stop retrying the function and it will return some error message from the website.
This works fine in my local machine but when I deploy my code to Vercel (running on AWS Lambda as far as I know), the setTimeout is being completely ignored, so the fetchData function keeps running until it gets the correct response or until the server default 60 seconds timeout triggers.
Here's the code:
router.get('/test', async (req, res) => {
try {
const browser = await playwright.launchChromium({
headless: false });
const context = await browser.newContext();
const page = await context.newPage();
let timeout = false;
let fetchDataTimeout = setTimeout(() => {
timeout = true;
}, 15000);
const fetchData = async () => {
await page.type('#code', '6824498040');
let data = await (
await Promise.all([
page.waitForResponse(
(response) =>
response.url() === `${env.API_URL2}` && response.status() === 200,
{ timeout: 10000 },
),
page.click('#btn-check'),
])
)[0].json();
if (data.errors && !timeout) {
await page.reload();
return await fetchData();
} else {
clearTimeout(fetchDataTimeout);
return data;
}
};
let data = await fetchData();
await browser.close();
res.json({
status: 200,
message: data,
});
} catch (error) {
console.error(error);
return res.status(500).send({ 'Server Error': `${error}` });
}
});
I read that you have to wrap the setTimeout into a Promise and return it as an async function. Like this:
const timeOut = async (t) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(`Completed in ${t}`)
}, t)
})
}
await timeOut(15000).then((result) => console.log(result))
But this will trigger the 15 seconds wait always. I need to discard the waiting if I get the correct response from fetchData and trigger the timeout if I don't 15 seconds after I start trying.
Any ideas???
The solution was quite simple. Since AWS Lambda requires an async function to properly wait the timeouts, I just had to wrap the timeout in a Promise. But then what did the trick was wrapping both functions, the timeout and the fetchData, in a Promise.race() method. By doing this, the promise that resolves first will stop the other one from running.
The code is now like this:
router.get('/test', async (req, res) => {
try {
const browser = await playwright.launchChromium({headless: false });
const context = await browser.newContext();
const page = await context.newPage();
const timeout = () =>
new Promise((resolve, reject) => {
setTimeout(() => {
reject('FUNCTION TIMEOUT');
}, 12000);
});
const fetchData = async () => {
await page.type('#code', '6824498040');
let response = await (
await Promise.all([
page.waitForResponse(
(res) =>
res.url() === `${env.API_URL2}` && res.status() === 200,
{ timeout: 10000 },
),
page.click('#btn-check'),
])
)[0].json();
if (data.error) {
await page.reload();
return await fetchData();
} else return data;
}
};
let data = await Promise.race([fetchData(), timeout()]);
await browser.close();
res.json({
status: 200,
message: data,
});
} catch (error) {
console.error(error);
return res.status(500).send({ 'Server Error': `${error}` });
}
});
I implemented a 12 seconds timeout instand of 15. I tested this on Vercel (AWS Lambda) and it works fine.
Thanks everybody, especially dandavis, this implementation was his idea.
Anyway, I hope it helps.
Related
Upon receptiton of a message from a websocket server, I would like to call a promise function and wait for a respond to decide if it needs to threat another messages or not.
I tried to wrap a connection.on('message', cb) into a Promise but it doesn't help either. Here is the code:
let alreadyBought = false
const client = new WebSocketClient()
client.connect('wss://...')
client.on('connect', async (connection) => {
connection.on('message', async event => {
if (event.type === 'utf8') {
const data = JSON.parse(event.utf8Data)
if (!alreadyBought) {
await trigger(data.p) // <--- not working
}
}
})
})
async function trigger(price) {
const order = await exchange.createLimitBuyOrder(config.currency, amount, price)
console.log(order)
alreadyBought = true
}
If I do a console.log(event), I get this, check a timestamp:
{
d: '{"t":1924698,"p":"1541.86", "T":1662043735929}'
}
{
d: '{"t":1924699,"p":"1541.86","T":1662043735955}' // <-- At the same timestamp
}
{
d: '{"t":1924700,"p":"1541.21","T":1662043735955}' // <-- At the same timestamp
}
{
d: '{"t":1924701,"p":"1540.91","T":1662043735955}' // <-- At the same timestamp
}
Ok, I finally found a solution, by using a pricelocks while a Promise is processing.
let pricelock = false
const client = new WebSocketClient()
client.connect('wss://...')
client.on('connect', async (connection) => {
connection.on('message', async event => {
if (event.type === 'utf8') {
const data = JSON.parse(event.utf8Data)
if (!pricelock) {
pricelock = true
await trigger(data.p).then(() => {
pricelock = false
})
}
}
})
})
async function trigger(price) {
await exchange.createLimitBuyOrder(config.currency, amount, price)
}
I will leave a post open in case there is a better solution.
I have a 2 identical Firebase functions that batch write data to Firestore. One is wrapped in a scheduled/onRun trigger, and the other is a HTTP onRequest trigger.
Both functions work fine and throw no errors.
They have the same amount of memory and timeout as well.
When invoking the http trigger, the function runs through and completes in about 30 seconds.
When invoking the scheduled onRun trigger, the function takes 5+ minutes to complete.
Is there something different about the runtimes that is not documented or something?
Edit: It works now - I made processMentions await totalMentions and return null.
processMentions does not have to return a promise, only a value because the actual scheduledPull/onRun function is returning the processMentions async function, which resolves the promise by returning a value.
Cheers for the help #dougstevenson
Triggers:
/**
* Get manual mentions
*/
exports.get = functions.https.onRequest((req, res) => {
const topic = 'topic'
const query = 'queryString'
processMentions(res, query, topic)
})
/**
* Get schedule mentions
*/
exports.scheduledPull = functions.pubsub.schedule('every day 1:00').onRun((context) => {
const topic = 'topic'
const query = 'queryString'
return processMentions('sched', query, topic)
})
Logic:
const functions = require('firebase-functions')
const admin = require('firebase-admin')
admin.initializeApp()
const db = admin.firestore()
const axios = require('axios')
const moment = require('moment')
// Globals
const auth = 'token'
const url = 'https://apiurl.com/'
async function totalMentions(nextPage, start, end, query) {
try {
let config = {
headers: {
Authorization: auth,
Accept: 'text/html',
}
}
const response = await axios.get(url, config)
const total = response.data.results.total
const loops = Math.ceil(total / 500)
return loops
} catch (error) {
console.log('error 1', error)
}
}
async function allMentions(nextPage, start, end, query) {
try {
let config = {
headers: {
Authorization: auth,
Accept: 'text/html',
},
}
const response = await axios.get(url, config)
return response
} catch (error) {
console.log('error 2', error)
}
}
async function saveData(response, end, topic) {
try {
let data = await response.data.results.clips
let batch = db.batch()
data.forEach((c) => {
delete c.localTime
let reff = db.collection(collection).doc(date).collection(collection).doc(c.id.toString())
batch.set(reff, c)
})
let batches = await batch.commit()
return batches
} catch (error) {
console.log('error3 ', error)
}
}
async function processMentions(res, query, topic) {
try {
totalMentions(1, start, end, query)
.then(async (loops) => {
let endbatch = 0
for (let i = 1; i <= loops; i++) {
await allMentions(i, start, end, query)
.then(async (response) => {
await saveData(response, end, topic)
return ++endbatch
})
.catch((err) => {
console.log('error 4 ' + err)
})
if (endbatch === loops) {
if (res !== 'sched') {
console.log('http trigger finished')
return res.status(200).end()
} else {
return console.log('schedule finished')
}
}
}
})
.catch((err) => {
console.log('error5 ' + err)
})
} catch (error) {
console.log('error6 ' + error)
}
}
For the pubsub trigger to work correctly, processMentions needs to return a promise that resovles when all of the async work is complete. Right now, it's returning nothing, which (since it's declared async) translates into a promise that's resolved immediately with no value. Calling then/catch on a promise isn't doing what you expect - you need to return a promise chain from your async work.
I'm not sure why you have it declared async, without also using await inside of it to manage the promises much more easily.
I tried to read HTTP response with axios and parse JSON in stream mode with stream-json` so I can fully fit my database on demand. It works well but if I try to close database connection everything will be crashed because the connection will be closed too soon.
The problem is: await doesn't wait for the extract_coins function to complete (even if it's returning the promise) and close database connection in a final scope.
const main = async() => {
const dbcfg = config.get('db.coins');
const db = await coins_db_configure(dbcfg);
try {
console.log('Connected to the database and created coins table!');
await extract_coins('some_url_which_gives_correct_json', coins_emitter);
}
catch(e){
console.error(e);
}
finally {
await db.close();
}
};
main();
extract_coins:
module.exports = async function extract_coins(url, emitter){
return await axios({
method: 'get',
url: url,
responseType: 'stream'
}).then((res) => {
const pipeline = chain([
res.data,
parser(),
pick({filter: 'data'}),
streamArray()
]);
pipeline.on('data', data => {
emitter.emit('coin_extracted', data.value);
});
pipeline.on('end', () => console.log("All the coins were successfully passed!"));
});
};
As the code related to the asynchronous pipeline is not promisified, you currently have no way to return a promise that resolves on getting the "end" event.
Your await does wait for the promise to resolve, but your then callback returns undefined, and so that resolves the promise at that very moment, long before the end event is broadcast.
So change this:
then((res) => {
const pipeline = chain([
res.data,
parser(),
pick({filter: 'data'}),
streamArray()
]);
pipeline.on('data', data => {
emitter.emit('coin_extracted', data.value);
});
pipeline.on('end', () => console.log("All the coins were successfully passed!"));
});
To this:
then((res) => new Promise((resolve) => {
const pipeline = chain([
res.data,
parser(),
pick({filter: 'data'}),
streamArray()
]);
pipeline.on('data', data => {
emitter.emit('coin_extracted', data.value);
});
pipeline.on('end', () => {
console.log("All the coins were successfully passed!");
resolve();
});
}));
Actually, you used ES6+ way but not in a common way, this causes this issue but, definitely, using await alongside then is exactly a fault, you should write them just like following codes:
const main = async () => {
try {
const dbcfg = config.get('db.coins');
const db = await coins_db_configure(dbcfg);
console.log('Connected to the database and created coins table!');
await extract_coins('some_url_which_gives_correct_json', coins_emitter);
} catch (e) {
console.error(e);
} finally {
await db.close();
}
};
main();
As you see, I put all of the codes inside try block, because using async/await means we pretend to write a sync code but it is async in fact, so we should put all codes, especially asynchronous lines inside try block. after putting them inside try block, just because of async flag behind of () JavaScript interpreter wait for each line to finish, if each line has error next lines won't run and interpreter falls into catch block, finally runs in all cases as you now.
Ok now back to the main issue, the extract_coins function:
export default (async function extract_coins(url, emitter) {
try {
const res = await axios({
method: 'get',
url: url,
responseType: 'stream'
});
const pipeline = chain([
res.data,
parser(),
pick({filter: 'data'}),
streamArray()
]);
await pipeline.on('data', data => {
emitter.emit('coin_extracted', data.value);
});
await pipeline.on('end', () => console.log("All the coins were successfully passed!"));
// HERE: return what you want
} catch (e) {
throw e;
}
});
This is the cause of the issue, you should pass the promise of extract_coins function with the new EcmaScript method, not using callback functions.
If I wan in your place I would write the extract_coins like below:
const extract_coins = async (url, emitter) => {
try {
const res = await axios({
method: 'get',
url: url,
responseType: 'stream'
});
const pipeline = chain([
res.data,
parser(),
pick({filter: 'data'}),
streamArray()
]);
await pipeline.on('data', data => {
emitter.emit('coin_extracted', data.value);
});
await pipeline.on('end', () => console.log("All the coins were successfully passed!"));
// HERE: return what you want
} catch (e) {
throw e;
}
};
export default extract_coins;
I have to do a functionality to test if 3 APIs are running.
Thus, the user will click on the Test APIs button and it will return the status of each API (status: 200, 500, 404 etc). If an API return an error, I should show the error stack.
Screen example:
API Status Detail
url1.com 200 -
url2.com 200 -
url3.com 500 internal server error
My question is, how can I call the 3 requests in parallel and return the async result, I mean how can I update the screen of API request status without having to wait for the result of all requests
I was basing on that How do I call three requests in order?, but it returns the result synchronously.
*******EDIT*****
Thats my current code
app.get('/testDependencies', function (req, res, next) {
let objTestsResul = {}
var urls = ['url1', 'url2', 'url3'];
let index = 0
while(urls.length > 0) {
let url = urls.shift();
objTestsResult[index++] = testURL(url)
}
res.send(objTestsResult)
});
This function is the same for each URL:
function testURL(URL){
fetch(URL, {
method: 'GET'
})
.then(res => {
res.json()
})
.then(json => {
console.log(json)
return json
})
.catch(error => {
return error
})
}
Promises (mdn) seem to be what you're looking for. They're essentially a more readable version of callbacks, which allow you to execute code when something else occurs rather than having to wait for that trigger to occur before resuming execution.
let endpoint1 = () => new Promise(resolve => setTimeout(() => resolve('200'), 1000));
let endpoint2 = () => new Promise(resolve => setTimeout(() => resolve('201'), 2000));
let endpoint3 = () => new Promise(resolve => setTimeout(() => resolve('500'), 1500));
document.getElementById('test').addEventListener('click', () => {
document.getElementById('status').textContent = 'test running...';
Promise.all([
endpoint1().then(a => document.getElementById('result1').textContent = a),
endpoint2().then(a => document.getElementById('result2').textContent = a),
endpoint3().then(a => document.getElementById('result3').textContent = a),
]).then(() => document.getElementById('status').textContent = 'test complete');
});
<button id="test">test</button>
<div>status: <span id="status">not running</span></div>
<div>endpoint 1: <span id="result1"></span></div>
<div>endpoint 2: <span id="result2"></span></div>
<div>endpoint 3: <span id="result3"></span></div>
This is actually pretty straightforward if you can use Bluebird:
const { Promise } = require('bluebird');
app.get('/testDependencies', function (req, res, next) {
Promise.map(['url1', 'url2', 'url3'], url => testURL(url)).then(results => {
res.send(results);
});
});
You'll just need to ensure your promise function actually returns a promise:
function testURL(URL) {
let start_time = new Date().getTime();
return fetch(URL, {
method: 'GET'
}).then(res => {
res.json()
}).then(json => {
console.log(json)
return json
}).catch(error => {
return error
})
}
Promises can't be dependency chained unless you explicitly return them from the function that's involved in chaining.
If you're able to use async and await, I'd also recommend doing that as well as that can vastly simplify otherwise complex code.
Express can't send multiple responses. You will have to finish all calls or use WebSockets to stream data.
function testURL(URL) {
return new Promise((resolve, reject) => {
if (URL === 'url2') {
reject(new Error('Internal Server Error'));
return;
}
resolve({ status: 200 });
});
}
const main = async () => {
const urls = ['url1', 'url2', 'url3'];
// return resolved and rejected Promises because if one fails in Promise.all
// the function will throw and we won't have any access to any resolved Promises.
const results = await Promise.all(urls
.map(url => testURL(url).then(response => response).catch(error => error)));
// every error have a stack property, Set the status to whatever you want
// based on the error and store the stack and the message
const objTestsResul = results.reduce((result, cur, i) => {
result[urls[i]] = cur.stack
? { status: 500, message: cur.message, stack: cur.stack }
: cur;
return result;
}, {});
console.log(objTestsResul);
};
main();
In my project I use promise (code below), how it is possible, that promise still pending, when I used keyword await. Can someone help me figure out, what I'm doing wrong?
const getTs = async () => {
const response = await axios.get('...')
.then(res => res.data)
.catch(() => 'ERROR');
return response;
};
console.log(getTs()); // Promise { <pending> }
The await does only stop the execution of the async function body, nothing else. The caller is not blocked, the code is still asynchronous, and you get back a promise. If you want to log the result, you have to wait for it.
const getTs = () => axios.get('...').then(res => res.data).catch(() => 'ERROR');
getTs().then(console.log);
// ^^^^^
or
async function getTs() {
try {
const res = await axios.get('...');
return res.data;
} catch (e) {
return 'ERROR';
}
}
async function main() {
const response = await getTs();
// ^^^^^
console.log(response)
}
main();
getTs will be resolved once the request is resolved. So you have to await the response like :
const getTs = async () => {
const response = await axios.get('...')
.then(res => res.data)
.catch(() => 'ERROR');
return response;
};
getTs().then(response => console.log(response));