Firebase transaction triggers function error? - firebase

Already spent quite a few hours trying to figure out the following, hopefully someone could point me.
In short what's happening: I have a firebase function that basically updates a database value in a transaction. But if I use transaction, the function always fail with this error:
Unhandled error RangeError: Maximum call stack size exceeded
at Function.isNull
Even though the transaction correctly updates the value in the database.
I was trying to debug it and remove anything I can. So whenever I remove transaction and use update() for example, the function finishes as expected with code 200.
As soon as I put transaction back the function fails with the following stacktrace:
Unhandled error RangeError: Maximum call stack size exceeded
at Function.isNull (/user_code/node_modules/firebase-functions/node_modules/lodash/lodash.js:11950:20)
at encode (/user_code/node_modules/firebase-functions/lib/providers/https.js:217:11)
at /user_code/node_modules/firebase-functions/node_modules/lodash/lodash.js:13402:38
at /user_code/node_modules/firebase-functions/node_modules/lodash/lodash.js:4911:15
at baseForOwn (/user_code/node_modules/firebase-functions/node_modules/lodash/lodash.js:2996:24)
at Function.mapValues (/user_code/node_modules/firebase-functions/node_modules/lodash/lodash.js:13401:7)
at encode (/user_code/node_modules/firebase-functions/lib/providers/https.js:242:18)
at /user_code/node_modules/firebase-functions/node_modules/lodash/lodash.js:13402:38
at /user_code/node_modules/firebase-functions/node_modules/lodash/lodash.js:4911:15
at baseForOwn (/user_code/node_modules/firebase-functions/node_modules/lodash/lodash.js:2996:24)
at Function.mapValues (/user_code/node_modules/firebase-functions/node_modules/lodash/lodash.js:13401:7)
Another similar stacktrace I've got:
Unhandled error RangeError: Maximum call stack size exceeded
at /user_code/node_modules/firebase-functions/node_modules/lodash/lodash.js:13402:38
at /user_code/node_modules/firebase-functions/node_modules/lodash/lodash.js:4911:15
at baseForOwn (/user_code/node_modules/firebase-functions/node_modules/lodash/lodash.js:2996:24)
at Function.mapValues (/user_code/node_modules/firebase-functions/node_modules/lodash/lodash.js:13401:7)
at encode (/user_code/node_modules/firebase-functions/lib/providers/https.js:242:18)
at /user_code/node_modules/firebase-functions/node_modules/lodash/lodash.js:13402:38
at /user_code/node_modules/firebase-functions/node_modules/lodash/lodash.js:4911:15
at baseForOwn (/user_code/node_modules/firebase-functions/node_modules/lodash/lodash.js:2996:24)
at Function.mapValues (/user_code/node_modules/firebase-functions/node_modules/lodash/lodash.js:13401:7)
at encode (/user_code/node_modules/firebase-functions/lib/providers/https.js:242:18)
Here is the function:
index.js:
const statsModule = require("./stats")
exports.myMethod9 = functions.https.onCall((data, context) => {
console.log("updateStatsMessagesChangedFeedbackHttp: data = ", data)
return statsModule.updateStatsMessagesChangedFeedbackHttp(data, context, admin);
});
stats.js
exports.updateStatsMessagesChangedFeedbackHttp = function (data, context, admin) {
var userId = "sdfsdf";
var feedback = data.feedback;
console.log("updateStatsMessagesChangedFeedbackHttp: userId = ", userId)
return exports.updateStatsMessagesChangedFeedback(userId, feedback, admin);
}
exports.updateStatsMessagesChangedFeedback = function (userId, feedback, admin) {
console.log("updateStatsMessagesChangedFeedback: feedback = ", feedback, ", userId = ", userId);
var root = admin.database().ref().root;
var path = constModule.statsUpdatedMessagesFeedbackPath + "/" + feedback;
var statsRef = root.child(path);
return statsRef.transaction(function (stats) {
if(!stats) {
stats = {votedUsers:{}, count: 0};
}
stats.votedUsers[userId] = true;
console.log("updateStatsMessagesChangedFeedback: stats = ", stats)
return stats;
}, function (error, committed, snapshot) {
//nothing wrong here - error is always null, committed - true
var snapVal = snapshot ? snapshot.val() : null;
console.log("error = ", error, ", committed = ", committed, ", data = ", snapVal);
if (error) {
// The fetch succeeded, but the update failed.
console.error("updateStatsMessagesChangedFeedback: The fetch succeeded, but the update failed: ", error);
} else {
console.log("updateStatsMessagesChangedFeedback: all ok: data = ", snapVal);
}
})
.catch(function (error) {
//this is never called - all good here as well
console.error("updateStatsMessagesChangedFeedback: error = ", error)
});
}
I call it from client web sdk like this:
var call = firebase.functions().httpsCallable('myMethod9');dFeedbackHttp
call({feedback: "some data"}).then(function (result) {...})
Neither catch, nor the transaction callback shows any errors. Though the function still fails only if I use transaction.
(click the image to enlarge)
Any ides on what's going on?

After discussion with support (very helpful guys by the way!) here is the solution:
one need to return a value from function in case of transaction:
exports.myMethod9 = functions.https.onCall((data, context) => {
console.log("updateStatsMessagesChangedFeedbackHttp: data = ", data)
return statsModule.updateStatsMessagesChangedFeedbackHttp(data, context, admin);
}).then(function(){
return {}
});
I don't have any clue on why returning a value makes any difference here as for example update() returns void which still makes the function complete with code 200.
Waiting on further comments form the support and meanwhile if anyone has any info on this feel free to share.

Related

Firebase cloud function error: Maximum call size stack size exceeded

I've made firebase cloud function which adds the claim to a user that he or she has paid (set paid to true for user):
const admin = require("firebase-admin");
exports.addPaidClaim = functions.https.onCall(async (data, context) => {
// add custom claim (paid)
return admin.auth().setCustomUserClaims(data.uid, {
paid: true,
}).then(() => {
return {
message: `Succes! ${data.email} has paid for the course`,
};
}).catch((err) => {
return err;
});
});
However, when I'm running this function: I'm receiving the following error: "Unhandled Rejection (RangeError): Maximum call stack size exceeded". I really don't understand why this is happening. Does somebody see what could cause what's getting recalled which in turn causes the function to never end?
Asynchronous operations need to return a promise as stated in the documentation. Therefore, Cloud Functions is trying to serialize the data contained by promise returned by transaction, then send it in JSON format to the client. I believe your setCustomClaims does not send any object to consider it as an answer to the promise to finish the process so it keeps in a waiting loop that throws the Range Error.
To avoid this error I can think of two different options:
Add a paid parameter to be able to send a JSON response (and remove the setCustomUserClaim if it there isn’t any need to change the user access control because they are not designed to store additional data) .
Insert a promise that resolves and sends any needed information to the client. Something like:
return new Promise(function(resolve, reject) {
request({
url: URL,
method: "POST",
json: true,
body: queryJSON //A json variable I've built previously
}, function (error, response, body) {
if (error) {
reject(error);
}
else {
resolve(body)
}
});
});

Am I doing Firestore Transactions correct?

I've followed the Firestore documentation with relation to transactions, and I think I have it all sorted correctly, but in testing I am noticing issues with my documents not getting updated properly sometimes. It is possible that multiple versions of the document could be submitted to the function in a very short interval, but I am only interested in only ever keeping the most recent version.
My general logic is this:
New/Updated document is sent to cloud function
Check if document already exists in Firestore, and if not, add it.
If it does exist, check that it is "newer" than the instance in firestore, if it is, update it.
Otherwise, don't do anything.
Here is the code from my function that attempts to accomplish this...I would love some feedback if this is correct/best way to do this:
const ocsFlight = req.body;
const procFlight = processOcsFlightEvent(ocsFlight);
try {
const ocsFlightRef = db.collection(collection).doc(procFlight.fltId);
const originalFlight = await ocsFlightRef.get();
if (!originalFlight.exists) {
const response = await ocsFlightRef.set(procFlight);
console.log("Record Added: ", JSON.stringify(procFlight));
res.status(201).json(response); // 201 - Created
return;
}
await db.runTransaction(async (t) => {
const doc = await t.get(ocsFlightRef);
const flightDoc = doc.data();
if (flightDoc.recordModified <= procFlight.recordModified) {
t.update(ocsFlightRef, procFlight);
console.log("Record Updated: ", JSON.stringify(procFlight));
res.status(200).json("Record Updated");
return;
}
console.log("Record isn't newer, nothing changed.");
console.log("Record:", JSON.stringify("Same Flight:", JSON.stringify(procFlight)));
res.status(200).json("Record isn't newer, nothing done.");
return;
});
} catch (error) {
console.log("Error:", JSON.stringify(error));
res.status(500).json(error.message);
}
The Bugs
First, you are trusting the value of req.body to be of the correct shape. If you don't already have type assertions that mirror your security rules for /collection/someFlightId in processOcsFlightEvent, you should add them. This is important because any database operations from the Admin SDKs will bypass your security rules.
The next bug is sending a response to your function inside the transaction. Once you send a response back the client, your function is marked inactive - resources are severely throttled and any network requests may not complete or crash. As a transaction may be retried a handful of times if a database collision is detected, you should make sure to only respond to the client once the transaction has properly completed.
You use set to write the new flight to Firestore, this can lead to trouble when working with transactions as a set operation will cancel all pending transactions at that location. If two function instances are fighting over the same flight ID, this will lead to the problem where the wrong data can be written to the database.
In your current code, you return the result of the ocsFlightRef.set() operation to the client as the body of the HTTP 201 Created response. As the result of the DocumentReference#set() is a WriteResult object, you'll need to properly serialize it if you want to return it to the client and even then, I don't think it will be useful as you don't seem to use it for the other response types. Instead, a HTTP 201 Created response normally includes where the resource was written to as the Location header with no body, but here we'll pass the path in the body. If you start using multiple database instances, including the relevant database may also be useful.
Fixing
The correct way to achieve the desired result would be to do the entire read->check->write process inside of a transaction and only once the transaction has completed, then respond to the client.
So we can send the appropriate response to the client, we can use the return value of the transaction to pass data out of it. We'll pass the type of the change we made ("created" | "updated" | "aborted") and the recordModified value of what was stored in the database. We'll return these along with the resource's path and an appropriate message.
In the case of an error, we'll return a message to show the user as message and the error's Firebase error code (if available) or general message as the error property.
// if not using express to wrangle requests, assert the correct method
if (req.method !== "POST") {
console.log(`Denied ${req.method} request`);
res.status(405) // 405 - Method Not Allowed
.set("Allow", "POST")
.end();
return;
}
const ocsFlight = req.body;
try {
// process AND type check `ocsFlight`
const procFlight = processOcsFlightEvent(ocsFlight);
const ocsFlightRef = db.collection(collection).doc(procFlight.fltId);
const { changeType, recordModified } = await db.runTransaction(async (t) => {
const flightDoc = await t.get(ocsFlightRef);
if (!flightDoc.exists) {
t.set(ocsFlightRef, procFlight);
return {
changeType: "created",
recordModified: procFlight.recordModified
};
}
// only parse the field we need rather than everything
const storedRecordModified = flightDoc.get('recordModified');
if (storedRecordModified <= procFlight.recordModified) {
t.update(ocsFlightRef, procFlight);
return {
changeType: "updated",
recordModified: procFlight.recordModified
};
}
return {
changeType: "aborted",
recordModified: storedRecordModified
};
});
switch (changeType) {
case "updated":
console.log("Record updated: ", JSON.stringify(procFlight));
res.status(200).json({ // 200 - OK
path: ocsFlightRef.path,
message: "Updated",
recordModified,
changeType
});
return;
case "created":
console.log("Record added: ", JSON.stringify(procFlight));
res.status(201).json({ // 201 - Created
path: ocsFlightRef.path,
message: "Created",
recordModified,
changeType
});
return;
case "aborted":
console.log("Outdated record discarded: ", JSON.stringify(procFlight));
res.status(200).json({ // 200 - OK
path: ocsFlightRef.path,
message: "Record isn't newer, nothing done.",
recordModified,
changeType
});
return;
default:
throw new Error("Unexpected value for 'changeType': " + changeType);
}
} catch (error) {
console.log("Error:", JSON.stringify(error));
res.status(500) // 500 - Internal Server Error
.json({
message: "Something went wrong",
// if available, prefer a Firebase error code
error: error.code || error.message
});
}
References
Cloud Firestore Transactions
Cloud Firestore Node SDK Reference
HTTP Event Cloud Functions

Calling getItem() on DynamoDB object from AWS Lambda, why doesn't my callback execute?

I'm trying to get an item from my DynamoDB database. The way my code is presently written, I fail to retrieve any data from DynamoDB. I must be doing something wrong, because as far as I can tell from my test, my callback is not being called.
I spent all day on this yesterday and have been tinkering with it unsuccessfully since I woke up this morning.
If anyone can provide insight into what I'm doing wrong here, I would be very grateful. Thanks to everyone in advance!
Final note: The timeout on the Lambda function itself is set to 5 minutes. So I don't think the Lambda function is timing out before the db query can return. When I run the function, it exits after only a moment.
const AWS = require('aws-sdk');
const dynamodb = new AWS.DynamoDB();
var response = null;
var test = false;
function getFromDB(callback) {
const params = {
TableName: process.env['DB_TABLE_NAME'] // evaluates to 'test-table',
Key: {
"id": {
S: postId // evaluates to a big string, pulling it in from an SNS message. Verified it with console.log(). It stores the expected value.
}
}
};
dynamodb.getItem(params, function(err, data) {
if (err) callback(data, true); // an error occurred
else callback(data, true); // successful response
});
}
getFromDB((data, isCalled) => {
response = data;
test = isCalled;
});
console.log(data); // evaluates to null
console.log(test); // evaluates to false
I Had faced similar issue.
I removed async in the statement below to resolve :
exports.handler = async (event,context)
I think what's going on is Lambda calls the function, but it's not going to wait for the call back, so it thinks it is done and exits.
I think I had a similar problem and resolved it by using Bluebird and async/await.
I can provide a snippet from my code if you need it
Have you loaded the SDK? I can't see it in your code snippet
// Load the AWS SDK for Node.js
var AWS = require('aws-sdk');
// Set the region
AWS.config.update({region: 'REGION'});
EDIT: Included region

Mongoose asynchronous .save and callback

This is one of those problem that you can explain but do not know how to fix it. I have a simple store method:
exports.store = async (req, res) => {
const mydata = new MyModel(req.body)
await mydata.save(function(err,user) {
res.location('/mydata/id/' + user._id)
})
res.status(201).json({ data: userdata })
}
When it runs, I get the following error:
events.js:182
throw er; // Unhandled 'error' event
^
Error: Can't set headers after they are sent.
at validateHeader (_http_outgoing.js:489:11)
at ServerResponse.setHeader (_http_outgoing.js:496:3)
at ServerResponse.header (.../node_modules/express/lib/response.js:730:10)
at ServerResponse.location (.../node_modules/express/lib/response.js:847:15)
at .../src/controllers/users-controller.js:26:13
at .../node_modules/mongoose/lib/model.js:3919:16
at .../node_modules/mongoose/lib/services/model/applyHooks.js:162:20
at _combinedTickCallback (internal/process/next_tick.js:131:7)
at process._tickCallback (internal/process/next_tick.js:180:9)
Process finished with exit code 1
I appears that the callback function runs separately and asynchronously because the res.status(201).json({ data: userdata }) seems to be producing the error and does not let me set the location header.
I've looked around for how to set the location header but none of the answers are like what I'm trying to do. This seems like something that should have been done before...? I'm looking for some help on how to do this...
You are mixing up two way of thinking :
Promises (with async/await in your case)
Callback
Use only one
try {
const user = await mydata.save();
res.location(`/mydata/id/${user._id}`);
// Other stuff ...
} catch(err) {
// Handle the errors
}
here you get an article about Promises into mongoose.

Bluebird Errors With Rethinkdb and Async

I recently changed my database from mongodb to rethinkdb. Since I'm getting some bluebird errors. So I guess I have to return my db queries differently.
Warning: a promise was created in a handler but was not returned from it
Code
It is hard to tell where the errors occur, thus I'm posting some examples of how my db queries are coded.
initialisation
exports.initDBandTables = (callback)->
db = self.getConfig().db
throw err "No DB Defined" if !db?
r.dbList().contains(db)
.do((databaseExists) ->
return r.branch(
databaseExists,
{ created: 0 },
r.dbCreate(db)
)
).run ()->
async.parallel
session: (next)->
self.ensureTable('sessions', null, next)
serverlogs: (next)->
self.ensureTable('serverlogs', null, next)
users: (next)->
self.ensureTable('users', null, next)
(err)->
return callback err if err?
return callback()
example get:
r.table('users').filter(newFilter).select(selector).limit(limit).sort({lastName:-1}).run (err, usersFound) ->
return callback err if err?
return callback null, usersFound
example create
r.table('users').insert(user).run (err, updatedUser)->
return callback err if err?
updatedUser = updatedUser || null
return callback null, updatedUser
Questions:
1) Is there a way to increase the verbosity? It is hard to investigate, without line numbers/ file/ or on which operation the error occurs.
2) The 1st error is related to promises which return undefined, but I'm returning my callbacks everywhere.
The issue was not with my code but with a module I used.
https://github.com/armenfilipetyan/express-session-rethinkdb/issues/8

Resources