1

In my parse server I have a class called Stats which contains the columns secondsPlayed (number) and timeScore (number)

I am using cloud code to update all the rows in the column timeScore

The code below works only when updating and saving 1 or 2 objects results.length. If Parse.Query returns more than 2 results the code crashes and I get the following error.

error: Failed running cloud function timeScore for user undefined with:
Input: {}
Error: {"code":101,"message":"Object not found."} functionName=timeScore, code=101, message=Object not found., , user=undefined
error: Error generating response. ParseError { code: 101, message: 'Object not found.' } code=101, message=Object not found.
error: Object not found. code=101, message=Object not found.

This is a problem as I need to update and save thousands of objects. What is the best and fastest way to do this?

Why does my code work for 2 objects but not for more than 2? How can I fix this?

Here is my code

    var _ = require("underscore");

Parse.Cloud.define("timeScore", function(request, response) {

    var query = new Parse.Query("Stats");
    query.greaterThan("secondsPlayed", 1000);
    query.find().then(function(results) {

        _.each(results, function(result) {
            var secondsPlayed = result.get("secondsPlayed") || 0;
            result.set("timeScore", secondsPlayed*2);

        });
        return Parse.Object.saveAll(results);

    }).then(function(results) {

        response.success(results);
    }, function(error) {

        response.error(error);
    }); });

Here is how I call it

#!/usr/bin/env node
var Parse = require("parse/node");
Parse.initialize("xx",   "xx");
Parse.serverURL = "http://randomapp.herokuapp.com/parse";
Parse.Cloud.run('timeScore');

UPDATE:

Below is my latest code. Everything works well except for the fact that I get the following error for no apparent reason.

heroku[router]: at=error code=H12 desc="Request timeout" method=POST path="/parse/functions/timeScore

I get the timeout error regardless of what batchSize I choose and I get it every 30 seconds. I get it a total of 5 times. After the 5th time I dont get it anymore. I get the 5th and last error at around 2.5 minutes into the process (30seconds*5). This error does not effect the process in any way. All 250k objects are updated and saved regardless of batchSize.

I thought maybe because I never call results.error or results.success the server thinks I am still doing some work and shows the error. However I updated my code as the following and I still get the timeout errors.

Also after each timeout error processBatch() is called once again from the beggining. Since I get 5 timeout errors processBatch() gets called 5 times. So after the 5th timeout error there is 5 processBatch() functions running simultaneously (I confirmed this with logs).

What is causing the heroku timeout errors I am getting? How do I fix it?

var _ = require("underscore");
Parse.Cloud.define("timeScore", function(request, response) {
var counter = 0;
function processBatch(query, batchSize, startingAt, process) {
    query.limit(batchSize);
    query.skip(startingAt);

    return query.find().then(results => {

        return process(results).then(() => results.length);
    }).then(length => {

        return (length === batchSize)? processBatch(query, batchSize, startingAt+length, process) : {};
    });
}

function setTimeScores(stats) {
        console.log("LENGTH " + stats.length);
    _.each(stats, stat => {

        counter ++;
        stat.set("timeScore", counter);

    });
    return Parse.Object.saveAll(stats);
}

var query = new Parse.Query("Stats");

processBatch(query, 2500, 0, setTimeScores).then(results => {
        response.success(results);
    }).catch(error => {
        response.error(error);
    });

});
Murat Kaya
  • 193
  • 1
  • 18
  • The code looks okay. Sometimes parse errors aren't very instructive. One guess is that either Stats (or an object related to stats via a relation) has an ACL that requires a permissioned user. Here's a debug step: don't run this code on "Stats". Make a brand new class with no access control. Give a single property, an integer value which you *increment by 1* in this code. Does it still break? – danh Jun 27 '18 at 14:32
  • Incidentally, there is a problem in the code that will prevent it from running on thousands of objects: the max limit on the query is 1000. I can show you how to fix that once the more basic problem is solved. – danh Jun 27 '18 at 14:40
  • Hi, ACL is public read and write for Stats and every Stats object. I don't think the problem is read and write permissions as the code does work when I set the query so that only 2 objects are returned. When 3 or more objects are returned I get the error. Could the solution have something to do with promises? http://docs.parseplatform.org/js/guide/#promises – Murat Kaya Jun 27 '18 at 14:56
  • Nope. Your promise code looks impeccable. Does the Stats object have any relations to other objects? That's where I've seen the mysterious error before: the save of the stats object succeeds, but the save of a related object that fails (parse implicitly saves an object's relations on save). My suggestion is to remove any doubt about the data model / permissions / or other errors in the data by trying this code on a brand new class that has no relations (no properties of any sort) except a single int property that you increment. – danh Jun 27 '18 at 17:49
  • Hi, I did what you suggested. Tried running the code on a new class with new columns and my code worked. You were right. I realized that the problem was the format of the data I imported to mongodb was incompatible with parse. Once I fixed that my code worked. Now the only issue remaining is the query limit. The maximum number of objects that can be returned from my code is 100 not even 1000. How can I remove the query limit so I can run the code on thousands of objects? – Murat Kaya Jun 28 '18 at 12:48

1 Answers1

2

To handle numbers of objects greater than the max query limit, build a more abstract function that uses query's limit() and skip() to cursor through the data:

function processBatch(query, batchSize, startingAt, process) {
    query.limit(batchSize);
    query.skip(startingAt);
    return query.find().then(results => {
        return process(results).then(() => results.length);
    }).then(length => {
        return (length === batchSize)? processBatch(query, batchSize, startingAt+length, process) : {};
    });
}

This says, get one, batchSize-long batch of objects specified by query, then do something with the retrieved objects, then, if there might be more, do the same thing again, skipping objects we've processed already.

Your process step looks like this:

function setTimeScores(stats) {
    _.each(stats, stat => {
        var secondsPlayed = stat.get("secondsPlayed") || 0;
        stat.set("timeScore", secondsPlayed*2);
    });
    return Parse.Object.saveAll(stats);
}

Call it like this:

let query = new Parse.Query("Stats");
query.greaterThan("secondsPlayed", 1000);
processBatch(query, 100, 0, setTimeScores);

EDIT in the context of a cloud function, call it like this...

Parse.Cloud.define("computeStats", function(request, response) {
    let query = new Parse.Query("Stats");
    query.greaterThan("secondsPlayed", 1000);
    processBatch(query, 100, 0, setTimeScores).then(results => {
        response.success(results);
    }).catch(error => {
        response.error(error);
    });
});
danh
  • 62,181
  • 10
  • 95
  • 136
  • Hi, I tried your code. Can you please check out my code posted above as an update and tell me why it doesnt work properly? My code only updates and saves 4204 objects each time and gives the error "heroku[router]: at=error code=H12 desc="Request timeout" method=POST path="/parse/functions/timeScore" 5 times during its run. Always at the same place. I indicated in my code when this error happens with console.log. This error appears 5 times on the logs and setTimeScores() is only run 5 times. After the 5th time the code just stops running without having updated all 250000 objects. – Murat Kaya Jun 29 '18 at 17:07
  • Looks like a timeout, not a code problem. Is this running as a web request or a scheduled job. A web request has timeouts built in. The other possibility is heroku imposing a resource limitation. One test would be try try different batch sizes. Will it get ~42 runs with 100? or ~420 with 10? These won't be exact because of different overhead for different batch sizes, but it would be useful to know if the behavior happens at roughly 42 batches, and repeatably so. – danh Jun 29 '18 at 20:27
  • @MuratKaya - I don't want to be a pest about this, but I'm waiting for you to realize that it is unwise to design a system that visits tens of thousands of records all the time, updating each with a function based on wall clock time. You're not breaking any laws of physics here, but you are on your way to breaking laws of economics. – danh Jun 29 '18 at 20:30
  • Hi, I know what you mean and understand your POV. I am just trying to make a different kind of leaderboard that is time dependent. On a traditional leaderboard you would have just scores. The early adapters of the game would always be a the top as their scores has added up over time and the new user cant compete with them as it takes a lot of time to accumulate big scores. With a time dependent scoring the new user can compete with the long term users. As the score is a fraction of time spend in app/time since they downloaded the app. – Murat Kaya Jul 02 '18 at 14:11
  • I was mistaken before the updated code above only updates and saves 1000 objects (equal to batchsize I set). The setTimeScores() is called 5 times but always for the same 1000 objects. If I set the batch size to 100 then the same thing happens. Only 100 objects are updated and saved and setTimeScores() is called 5 times (gets recalled after the timeout error) but always for the same 100 objects. So no matter the batch size only one batch is updated and saved with the above code. – Murat Kaya Jul 02 '18 at 14:13
  • Also I found out that there is no query limit on parse server. I set 'query.limit(999999999)' on my original code and all 250000 objects were ruturned from parse however I got the same error on the console ("heroku[router]: at=error code=H12 desc="Request timeout" method=POST path="/parse/functions/timeScore") This error I mostly shows when parse attempts to save and is probably caused by heroku. Maybe it cant handle it. When I decrease the query.limit to 10000 my original code works flawlessly. – Murat Kaya Jul 02 '18 at 14:14
  • I am now trying to get rid of the timeout error by making my code run on a worker dyno instead of a web dyno and see if that solves the problem. However I am not able to do it. I did post a question about it. I would appreciate it if you could take a look at it. https://stackoverflow.com/questions/51118741/how-do-i-make-my-cloud-code-run-on-my-worker-dyno-instead-of-web-dyno – Murat Kaya Jul 02 '18 at 14:17
  • @MuratKaya - Okay, a few things: I'm not suggesting that you abandon the functionality, just that you perform the score calculation when someone asks for the records. Consider a server function that clients call to get the handful of Stats objects they want to present. The server gets *only those* and performs this calc on *only those*. This is economical. If 1 billion customers simultaneously request, you'll be happy to have so many customers, and you'll be rich. Spend your server money then! – danh Jul 02 '18 at 15:06
  • I'm puzzled by reading the same 1k twice. Can you double check your code to see that your are calculating the `startingAt` as I suggest and calling `.skip()`? – danh Jul 02 '18 at 15:07
  • You're right about 1k limit. Parse lifted that limit when they went open source. I'll look at the other question. The reason they had that limit in the first place is to not allow customers of the BaaS to burn resources that they were paying for. Now that you're paying, they are allowing you to spend as you wish. – danh Jul 02 '18 at 15:10
  • Hi, regarding score calculation since any of the 250k users could have a high timeScore I need to calculate timeScore for each user server side and then display the top 100 values of the calculated timeScore column to the client. Top 100 can change anytime and any 250k users could be in it. I cant just choose a handful of Stats objects (users) since any user can be in the top 100 so I have to calculate and populate timeScore for all of them to see whos is in Top 100. – Murat Kaya Jul 02 '18 at 18:13
  • startingAt is always 0 and is never calculated. setTimeScores() is recalled for the same batch over and over again. It gets recalled because of the "Request Timeout" error I get after the first batch is saved. App never moves on to the second batch. The app gives up after the 5th "Request Timeout" error and changes its state to completed. The request timeout error happens during this line `return (length === batchSize)? processBatch(matchingQuery, startingAt+length, process) : {};` – Murat Kaya Jul 02 '18 at 18:14
  • I see. The timeout is the underlying problem then. Reducing the batch size might make this work without any other code change. – danh Jul 02 '18 at 19:12
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/174187/discussion-between-danh-and-murat-kaya). – danh Jul 02 '18 at 19:14
  • Hi changing the line in your code to the following solved the problem `(length === batchSize)? processBatch(query, 100, startingAt+length, process) : {}; `Your code works without isssues now. Thank you. I do get the occasional timeout error if I select a large batchSize (10000) Now if only I could make the code run on a worker dyno. – Murat Kaya Jul 02 '18 at 19:19
  • Hi, I discovered new things with your code and wrote to you on chat. Can you take a look please? – Murat Kaya Jul 10 '18 at 06:32
  • Yes, you should call results success after the last promise fulfills. – danh Jul 10 '18 at 13:45
  • Where should I put it exactly? Can you edit your answer to show me? Much appreciated. – Murat Kaya Jul 11 '18 at 04:34
  • Hi, I implemented your last modification. This did not get rid of the timeout errors. I have updated this question to reflect my latest code. I am still trying to fix this timeout error. – Murat Kaya Jul 11 '18 at 07:32
  • I created a new question for the timeout error I am getting if you want to take a look. https://stackoverflow.com/questions/51377100/i-am-getting-the-heroku-h12-request-timeout-error-for-no-apparent-reason – Murat Kaya Jul 17 '18 at 10:12