Search code examples
parse-platformjobsparse-cloud-code

Parse background job query iterations are returning before fetching data


I'm trying to run some statistics on some of my Parse data to get some idea of usage statistics so far.

Basically I'm sorting a query for properties based on the date that they were created, and trying to iterate through each one to see if the user that created it is a new user for that area, and whether they're a billable user (someone who has entered payment information). Users can be customers of multiple zip codes, and can become customers of a new zip code at any time after creating their account, which complicates how I have to gather this data. Sorry for the messy code, I tried to add some comments to show what's going on. search for "//HERE" to see where the issue starts. (updated code below)

Parse.Cloud.job("runStatistics", function(request, status)
{
    Parse.Cloud.useMasterKey();
    // These are variables I'm outputting to see the behaviour of this background job.  fetchedProperties and 
    // fetchAttempts are both the same, and equal to the total number of properties, but the rest all remain 0.
    var failedUsers = 0;
    var successfulUsers = 0;
    var skippedUsers = 0;
    var nonSkippedUsers = 0;
    var propertyFetchErrors = 0;
    var fetchedProperties = 0;
    var fetchAttempts = 0;
    var totalProperties;

    // These are associative arrays  or arrays (key being the zip code) where I store whether or not someone 
    // is already a user for a zip code, or if they have requested a cut (purchasing from our app)
    var usersForZIPCode = {};
    var cutsForZIPCode = {};

    //I create a statistics object for each zip code for each week
    var Statistics = Parse.Object.extend("Statistics", 
    {
        initialize: function(attrs, options)
        {
            this.newUsers = 0;
            this.newBillableUsers = 0;
            this.firstCut = 0;
            this.additionalCuts = 0;
            this.numCuts = 0;
            this.totalBillableUsers = 0;
        }
    });
    var statisticsArray = new Array();
    var i = 0;
    for( i = 0; i < serviceableZIPCodesArray.length; i++ ) //ServiceableZIPCodesArray is an array of the zip codes we currently service, defined elsewhere.
    {
        usersForZIPCode[ serviceableZIPCodesArray[i] ] = new Array(); 
        cutsForZIPCode[ serviceableZIPCodesArray[i] ] = new Array();
        var j = 1;
        for( j = 1; j < 4; j++ )
        {
            var statistics = new Statistics();
            statistics.set("zipCode", serviceableZIPCodesArray[i]);
            statistics.set("week", j);
            statisticsArray.push(statistics);
        }
    }

    //Here I set up the property query. I have less than 1000 properties at the moment.
    var propertyQuery = new Parse.Query("Property");
    propertyQuery.limit(1000);
    propertyQuery.ascending("createdAt");
    propertyQuery.find( 
    {
        success: function(results)
        {
            totalProperties = results.length; //This is properly set
            for( var x = 0; x < results.length; x++)
            {
                var property = results[x];
                fetchAttempts++; //This is hit every time
                property.fetch( //HERE
                {
                    success: function(property)
                    {
                        fetchedProperties++; //This is never called, always returns 0.
                        dateCreated = property.createdAt;
                        var weekNum;
                        var newUserBool = false;
                        var billableUserBool = false;
                        var ZIPIndex = serviceableZIPCodesArray.indexOf( property.get("zip") );
                        if( serviceableZIPCodesArray.indexOf( property.get("zip") ) == -1 )
                        {
                            skippedUsers++; //this is never called, always returns 0
                        } 
                        else
                        {
                            nonSkippedUsers++; //this is never called, always returns 0

                            //These look a bit messy. Basically I'm using the current property's zip code as a key to get an
                            //array of users that already have properties in that zip code, so I don't count them twice
                            if( usersForZIPCode[ property.get("zip") ].indexOf( property.get("User") ) == -1 )
                            {
                                usersForZIPCode[ property.get("zip") ].push( property.get("User") );
                                newUserBool = true; //If the index was -1, they are a new user.
                            }
                            property.get("User").fetch( //User is a pointer to a User object that owns this property
                            {
                                success: function(user)
                                {
                                    successfulUsers++; //this is never called, always returns 0
                                   if( user.has(/* removing this in case it's a security issue*/) ) billableUserBool = true; 
                                   //This tells us which week the property was created: 1, 2, or 3.
                                   if( dateCreated.getDate() < 18 ) weekNum = 1; 
                                    else if( dateCreated.getDate() < 25 ) weekNum = 2;
                                    else weekNum = 3;
                                    //Based on which week the object was created, we update the statistics object
                                    switch(weekNum)
                                    {
                                        case 1:
                                            if( newUserBool )
                                            {
                                                if( billableUserBool )
                                                {
                                                    statisticsArray[ ZIPIndex*3 ].increment("newBillableUsers");
                                                    statisticsArray[ ZIPIndex*3 ].increment("newUsers");
                                                    statisticsArray[ ZIPIndex*3 ].increment("totalBillableUsers");
                                                    statisticsArray[ ZIPIndex*3 + 1 ].increment("totalBillableUsers");
                                                    statisticsArray[ ZIPIndex*3 + 2 ].increment("totalBillableUsers");
                                                }
                                                else
                                                {
                                                    statisticsArray[ ZIPIndex*3 ].increment("newUsers");
                                                }
                                            }
                                            break;
                                        case 2:
                                            if( newUserBool )
                                            {
                                                if( billableUserBool )
                                                {
                                                    statisticsArray[ ZIPIndex*3 + 1 ].increment("newBillableUsers");
                                                    statisticsArray[ ZIPIndex*3 + 1 ].increment("newUsers");
                                                    statisticsArray[ ZIPIndex*3 + 1 ].increment("totalBillableUsers");
                                                    statisticsArray[ ZIPIndex*3 + 2 ].increment("totalBillableUsers");
                                                }
                                                else
                                                {
                                                    statisticsArray[ ZIPIndex*3 + 1 ].increment("newUsers");
                                                }
                                            }
                                            break;
                                        case 3:
                                            if( newUserBool )
                                            {
                                                if( billableUserBool )
                                                {
                                                    statisticsArray[ ZIPIndex*3 + 2 ].increment("newBillableUsers");
                                                    statisticsArray[ ZIPIndex*3 + 2 ].increment("newUsers");
                                                    statisticsArray[ ZIPIndex*3 + 2 ].increment("totalBillableUsers");
                                                }
                                                else
                                                {
                                                    statisticsArray[ ZIPIndex*3 + 2 ].increment("newUsers");
                                                }
                                            }
                                            break;
                                        default:
                                    }

                                },
                                error: function(user, error)
                                {
                                    failedUsers++; //this is never called, always returns 0
                                }
                            }).then(
                            function()
                            {
                                    successfulUsers++;
                            },
                            function(error)
                            {
                                    failedUsers++; //this is never called, always returns 0
                            });
                        }
                    },
                    error: function(property, error)
                    {
                        propertyFetchErrors++; //this is never called, always returns 0
                    }
                }).then(
                function(property)
                {
                    fetchedProperties++; //this is never called, always returns 0
                },
                function(error)
                {
                    propertyFetchErrors++; //this is never called, always returns 0
                });
            }
        },
        error: function(results, error)
        {
            status.error("Uh oh, something went wrong with the query" + error); 
        }
    }).then(
        function()
        {
            console.log("failed users = " + failedUsers);
            console.log("successful users = " + successfulUsers);
            console.log("skipped users = " + skippedUsers);
            console.log("nonSkipped users = " + nonSkippedUsers);
            console.log("total properties = " + totalProperties);
            console.log("fetch attempts = " + fetchAttempts);
            console.log("property fetch errors = " + propertyFetchErrors);
            console.log("fetched properties = " + fetchedProperties);
            Parse.Object.saveAll(statisticsArray).then( 
            function()
            {
                status.success("created statistics objects");
            }, function(error)
            {
                status.error("Uh oh, something went wrong while saving." + error);
            });
        },
        function(error)
        {
            status.error("something went wrong with the property query" + error);
    });
});

I'm sorry it's so long and messy, if you think I should update this without most of that code that doesn't get reached, let me know. I just remember reading some documentation about promises saying they had different behaviour when the function you were calling a promise after returns a promise. I thought that the inner function that returns a promise has to finish first, so I'd be protected here, but clearly I'm mistaken, as my fetches never get called.

I appreciate any help! I've been stuck on this for hours now.

edit - I've changed the code to only use promises, rather than a mix of call backs and promises. It turns out I did not need to fetch the property again, as the query did already fetch it. I must have had a misspelled variable name or something that gave me a null object before.

However, now my problem is that the user is not being fetched. It's basically the same problem as before, just in a different spot since I didn't actually have to do the original fetch. Here is my updated code:

Parse.Cloud.job("runStatistics", function(request, status)
{
    Parse.Cloud.useMasterKey();
    // These are variables I'm outputting to see the behaviour of this background job.  fetchedProperties and 
    // fetchAttempts are both the same, and equal to the total number of properties, but the rest all remain 0.
    var failedUsers = 0;
    var successfulUsers = 0;
    var skippedUsers = 0;
    var nonSkippedUsers = 0;
    var propertyFetchErrors = 0;
    var fetchedProperties = 0;
    var fetchAttempts = 0;
    var totalProperties;

    // These are associative arrays  or arrays (key being the zip code) where I store whether or not someone 
    // is already a user for a zip code, or if they have requested a cut (purchasing from our app)
    var usersForZIPCode = {};
    var cutsForZIPCode = {};

    //I create a statistics object for each zip code for each week
    var Statistics = Parse.Object.extend("Statistics", 
    {
        initialize: function(attrs, options)
        {
            this.newUsers = 0;
            this.newBillableUsers = 0;
            this.firstCut = 0;
            this.additionalCuts = 0;
            this.numCuts = 0;
            this.totalBillableUsers = 0;
        }
    });
    var statisticsArray = new Array();
    var i = 0;
    for( i = 0; i < serviceableZIPCodesArray.length; i++ ) //ServiceableZIPCodesArray is an array of the zip codes we currently service, defined elsewhere.
    {
        usersForZIPCode[ serviceableZIPCodesArray[i] ] = new Array(); 
        cutsForZIPCode[ serviceableZIPCodesArray[i] ] = new Array();
        var j = 1;
        for( j = 1; j < 4; j++ )
        {
            var statistics = new Statistics();
            statistics.set("zipCode", serviceableZIPCodesArray[i]);
            statistics.set("week", j);
            statisticsArray.push(statistics);
        }
    }

    //Here I set up the property query. I have less than 1000 properties at the moment.
    var propertyQuery = new Parse.Query("Property");
    propertyQuery.limit(1000);
    propertyQuery.ascending("createdAt");
    propertyQuery.find().then( 
        function(results)
        {
            totalProperties = results.length; //This is properly set
            for( var x = 0; x < results.length; x++)
            {
                var property = results[x];
                fetchAttempts++; //This is hit every time
                        fetchedProperties++; //obviously, this now == fetchAttemps
                        dateCreated = property.createdAt;
                        var weekNum;
                        var newUserBool = false;
                        var billableUserBool = false;
                        var ZIPIndex = serviceableZIPCodesArray.indexOf( property.get("zip") );
                        if( serviceableZIPCodesArray.indexOf( property.get("zip") ) == -1 )
                        {
                            skippedUsers++; //this gets set.
                        } 
                        else
                        {
                            nonSkippedUsers++; //this gets set

                            //These look a bit messy. Basically I'm using the current property's zip code as a key to get an
                            //array of users that already have properties in that zip code, so I don't count them twice
                            if( usersForZIPCode[ property.get("zip") ].indexOf( property.get("User") ) == -1 )
                            {
                                usersForZIPCode[ property.get("zip") ].push( property.get("User") );
                                newUserBool = true; //If the index was -1, they are a new user.
                            }
                            property.get("User").fetch().then( //User is a pointer to a User object that owns this property
                            function(user)
                            {
                                    successfulUsers++;
                                    if( user.has(/* removing this in case it's a security issue*/) ) billableUserBool = true; 
                                   //This tells us which week the property was created: 1, 2, or 3.
                                   if( dateCreated.getDate() < 18 ) weekNum = 1; 
                                    else if( dateCreated.getDate() < 25 ) weekNum = 2;
                                    else weekNum = 3;
                                    //Based on which week the object was created, we update the statistics object
                                    switch(weekNum)
                                    {
                                        case 1:
                                            if( newUserBool )
                                            {
                                                if( billableUserBool )
                                                {
                                                    statisticsArray[ ZIPIndex*3 ].increment("newBillableUsers");
                                                    statisticsArray[ ZIPIndex*3 ].increment("newUsers");
                                                    statisticsArray[ ZIPIndex*3 ].increment("totalBillableUsers");
                                                    statisticsArray[ ZIPIndex*3 + 1 ].increment("totalBillableUsers");
                                                    statisticsArray[ ZIPIndex*3 + 2 ].increment("totalBillableUsers");
                                                }
                                                else
                                                {
                                                    statisticsArray[ ZIPIndex*3 ].increment("newUsers");
                                                }
                                            }
                                            break;
                                        case 2:
                                            if( newUserBool )
                                            {
                                                if( billableUserBool )
                                                {
                                                    statisticsArray[ ZIPIndex*3 + 1 ].increment("newBillableUsers");
                                                    statisticsArray[ ZIPIndex*3 + 1 ].increment("newUsers");
                                                    statisticsArray[ ZIPIndex*3 + 1 ].increment("totalBillableUsers");
                                                    statisticsArray[ ZIPIndex*3 + 2 ].increment("totalBillableUsers");
                                                }
                                                else
                                                {
                                                    statisticsArray[ ZIPIndex*3 + 1 ].increment("newUsers");
                                                }
                                            }
                                            break;
                                        case 3:
                                            if( newUserBool )
                                            {
                                                if( billableUserBool )
                                                {
                                                    statisticsArray[ ZIPIndex*3 + 2 ].increment("newBillableUsers");
                                                    statisticsArray[ ZIPIndex*3 + 2 ].increment("newUsers");
                                                    statisticsArray[ ZIPIndex*3 + 2 ].increment("totalBillableUsers");
                                                }
                                                else
                                                {
                                                    statisticsArray[ ZIPIndex*3 + 2 ].increment("newUsers");
                                                }
                                            }
                                            break;
                                        default:
                                    }
                            },
                            function(error)
                            {
                                    failedUsers++; //this is never called, always returns 0
                            });
                        }
            }
        },
        function(results, error)
        {
            status.error("Uh oh, something went wrong with the query" + error); 
        }
    ).then(
        function()
        {
            console.log("failed users = " + failedUsers);
            console.log("successful users = " + successfulUsers);
            console.log("skipped users = " + skippedUsers);
            console.log("nonSkipped users = " + nonSkippedUsers);
            console.log("total properties = " + totalProperties);
            console.log("fetch attempts = " + fetchAttempts);
            console.log("property fetch errors = " + propertyFetchErrors);
            console.log("fetched properties = " + fetchedProperties);
            Parse.Object.saveAll(statisticsArray).then( 
            function()
            {
                status.success("created statistics objects");
            }, function(error)
            {
                status.error("Uh oh, something went wrong while saving." + error);
            });
        },
        function(error)
        {
            status.error("something went wrong with the property query" + error);
    });
});

Solution

  • Don't mix promises with callbacks, choose 1 of the 2 approaches and stick with it. Mixing and matching generally means something gets dropped and your function exits early without calling the status handler.

    Using promises will help you break the code up so its easier to follow.

    You shouldn't need to run the fetch after just running the find because the query should return all column values for each object.

    For future use you may want to consider using .each instead of .find.

    When using promises you need to chain them and return the nested promises:

    query.find().then(function(x) {
        ... // basic logic
        return object.save(); // return when creating a promise
    }).then( function(y) {
        ...
    }) ...
    

    Note, you can chain on the save but you need to return the 'head' of the promise chain.