7

I'm building an Alexa Skill, which requires me to listen to a Firebase Realtime Database. In one particular part of the skill, I need to write a JSON object to Firebase, consisting of two fields, "intent", with an insignificant value, and "done", with a value of false.

Then, I wait for another device listening to this database to register this change, at which point it creates another field, named "result", with some numerical value, and changes the value of "done" to true.

Then the original function (test1) should recognize when the value of "done" is true, and then retrieve the value of "result".

What I'm having trouble with is coming up with a function that does all of these read/write operations before my main (asynchronous) function finishes. As the title suggests, AWS Lambda times out for some reason, and I am unable to read the value of "result".

This is the function I'm using:

function test1(intentName, targetRef, context) {
    console.log("writing");
    targetRef.set({
        intent: intentName,
        done: false
    }).then(function() {
        return targetRef.orderByChild("done").equalTo(true).on("value");
    }).then(function(snapshot) {
        var res = snapshot.val().result;
        console.log("Res: " + res);
        context.succeed( //context.succeed should be called after "result" has a value.
            generateResponse(
                buildSpeechletReponse("The result is" + processNumbersForSpeech(res), true),
                {}
            )
        );
    });
}

This is the output of the console (in AWS Lambda):

    
20:05:31
START RequestId: a25d2354-d9cb-11e6-b80a-f35142a5f45f Version: $LATEST
20:05:31
2017-01-13T20:05:31.464Z    a25d2354-d9cb-11e6-b80a-f35142a5f45f    writing

20:05:35
END RequestId: a25d2354-d9cb-11e6-b80a-f35142a5f45f

20:05:35
REPORT RequestId: a25d2354-d9cb-11e6-b80a-f35142a5f45f  Duration: 4001.15 ms    Billed Duration: 4000 ms Memory Size: 128 MB    Max Memory Used: 39 MB

20:05:35
2017-01-13T20:05:35.335Z a25d2354-d9cb-11e6-b80a-f35142a5f45f Task timed out after 4.00 seconds

The following is the structure of the Firebase data:

enter image description here

"done" is initially false. When the other device add "result", it also updates the value of "done" to true. "148434459..." is targetRef.

Your help is truly appreciated. I will supply more info if needed.

Miki P
  • 652
  • 1
  • 9
  • 22

5 Answers5

3

The problem is that on does not return a promise. Instead, it returns the callback function that was passed as a parameter. on will invoke the callback for the initial data and then again for any changes made to the data.

You most likely want to use once, which does return a promise:

...
}).then(function () {
    return targetRef.orderByChild("done").equalTo(true).once("value");
}).then(function (snapshot) {
    snapshot.forEach(function (childSnapshot) {
        console.log(childSnapshot.val());
    });
})
...

Note that the promise's resolved snapshot will contain zero or more children that matched the query. To enumerate the children, you can use the snapshot's forEach method.

cartant
  • 57,105
  • 17
  • 163
  • 197
  • When I do this, the console prints: "Unread: null", and then "FIREBASE WARNING: Using an unspecified index. Consider adding ".indexOn": "done" at targetRef to your security rules for better performance – Miki P Jan 13 '17 at 21:27
  • 2
    Updated the answer to show how to enumerate the matching children. Calling `val()` on the snapshot returns an object containing the child keys and values - so there's no `result` key in it. The warning is because you have not indexed the child property (`done`) you are using in the query. For that, see the [docs](https://firebase.google.com/docs/database/security/indexing-data). – cartant Jan 13 '17 at 21:39
  • I'm a bit confused... So will I be able to read the value of "result" ? And also, when I call console.log(childSnapshot.val()), there's no output. – Miki P Jan 13 '17 at 21:48
  • Have a look at the console output and it should make sense. I imagine it's `childSnapshot.val().result`, but your question doesn't make your database structure clear. Also, add a `console.log(snapshot.val())` to see whether or not you have any matching children. – cartant Jan 13 '17 at 21:51
  • I tried calling console.log("hello") right before console.log(childSnapshot.val()), and it's not outputting "hello" either. I'm not sure why, but it doesn't seem to execute the forEach block. – Miki P Jan 13 '17 at 22:01
  • Then you don't have any matching children. See my previous comment and log `snapshot.val()`. If it's an empty object, there are no children. Or log `snapshot.numChildren()`. Or look at your data in the Firebase console to verify that there are children, etc. – cartant Jan 13 '17 at 22:13
  • Yep. snapshot.val() logs as null. Then how would I go about reading data from targetRef only when done is true, so I can obtain the value of "result"? My other device adds "result" before it sets "done" to true. I also added an image of how my data is structured, to clear that part up. – Miki P Jan 14 '17 at 05:14
2

I found out the hard way that initializing firebase in a lambda function (e.g. via firebase-admin) will prevent the function from terminating (until the default 6-second timeout) unless you call app.delete() on the firebase app instance. Additionally, you should make sure that every time your run your lambda function you are using a new firebase app instance otherwise you will run into problems.

const app = admin.initializeApp({});
app.database().ref('child').set('hello').then(() => {
  return app.delete();
}).then(() => {
  let response = {}; // whatever
  callback(null, response);
});
mattwindwer
  • 929
  • 9
  • 18
  • I see... That's very interesting. Does this apply even if I'm not using firebase-admin? – Miki P Jan 21 '17 at 20:46
  • Also, what do you by "use a new firebase app instace?" Should I call `firebase.initializeApp(config)` every time before I run the lambda function? Currently, I'm not doing this. – Miki P Jan 21 '17 at 22:12
  • @MikiP I believe the crux of the issue is that `initializeApp` (whether using firebase-admin or not) creates a persistent connection with firebase that prevents the lambda process from exiting unless the connection is terminated via `app.delete()`. So you need to cleanup your firebase connections. And yes you should call `initializeApp` every time in your lamba function otherwise you may run into a "this app was already deleted" error if you're initializing the firebase outside the function context. – mattwindwer Jan 23 '17 at 22:14
  • I see. Thank you very much for your help. – Miki P Jan 25 '17 at 01:53
0

Your Lambda function probably times out after 4 seconds because you set this value when configuring the Lambda function. In order for the your function to wait for the external thing to happen, you can schedule a function to query the value regularly with setTimeout() until the value is there. If that takes longer than 4 seconds, you need to increase the functions timeout, too.

Digitalkapitaen
  • 2,353
  • 13
  • 17
  • Unfortunately I've tried setting the timeout to upwards of 20 seconds, and it doesn't help. In the Firebase console, I see that "result" appears in less than a second. – Miki P Jan 13 '17 at 21:25
0

From the AWS Lambda documentation:

Q: How long can an AWS Lambda function execute?

All calls made to AWS Lambda must complete execution within 300 seconds. The default timeout is 3 seconds, but you can set the timeout to any value between 1 and 300 seconds.

I would suggest that you are going to have problems trying to wait for Firebase events from inside a Lambda function. Lambda is designed to be called with data and process data and exit, not to wait for other events to happen. You would be better off using some kind of VPS and running a general Node process to do the database work. Lambda pushing data into Firebase is fine, but what you are trying to achieve with Firebase might be the wrong approach.

Community
  • 1
  • 1
  • Ok. I just tried putting a timeout of 2 seconds, and the Firebase was updated with "result" before then. Is that still problematic? – Miki P Jan 13 '17 at 21:32
-2

You are calling the function before the other function is being called. Try npm sleep() or setTimeout() to give your function a pause.