timeoutparse-platformrequest-timed-out

Cloud code - "Execution timed out" during saveAll call in afterSave


Situation

Current implementation

On Android client

  1. When there is a new message, a new messageRequest of the MessageRequest class is created
  2. If the message should be direct, the messageRequest has type 0, otherwise type 1
  3. If the message is direct, there is recipient stored in the messageRequest object
  4. The messageRequest object is stored to parse.com

On parse.com back-end

In the afterSave of the MessageRequest it is checked if the message is direct or public and based on that

In both cases, the data like content, type, etc. are copied from messageRequest object into the newly created message object(s).

The reason for creating separate message for each user is that each user can have it in another status (unread, read, deleted).

The status column representing the unread, read, deleted status is set (by unread) for the message object.

Problem

When I call the ParseObject.saveAll method in the afterSave of MessageRequest, I get the Execution timed out - Request timed out error

I think the cause is that there are some limits on time in which the request must complete in cloud code. In my case, I'm creating ca 100 Messages for 1 MessageRequest

This doesn't seem so much to me, but maybe I'm wrong.

Source code

var generateAnnouncement = function(messageRequest, recipients) {

    var messageList = [];

    for (var i = 0; i < recipients.length; i++) {
        var msg = new Message();
        msg.set("type", 1);
        msg.set("author", messageRequest.get("author"));
        msg.set("content", messageRequest.get("content"));
        msg.set("recipient", recipients[i]);
        msg.set("status", 0)

        messageList.push(msg);
    }

    Parse.Object.saveAll(messageList).then(function(list) {
    }, function(error) {
        console.error(error.message);
    });

}

Parse.Cloud.afterSave("MessageRequest", function(request) {
    var mr = request.object;
    var type = mr.get("type");

    if (type == 0) {
        generateDirectMessage(mr);
    } else {
        var query = new Parse.Query(Parse.User);
        query.notEqualTo("objectId", mr.get("author").id);
        query.find().then(function(allUsersExceptAuthor) {
            generateAnnouncement(mr, allUsersExceptAuthor);
        }, function(error) {
            console.error(error.message);
        });
    }
});

How would you suggest to solve this?

Additional thoughts

Update - mockup representing user's "Inbox"

Inbox mockup

In the "inbox" user sees both direct messages and public announcements. They are sorted by chronological order.

Update #2 - using arrays to identify who viewed and who marked as deleted

Then my query for all messages not deleted by currently logged in user looks like this

//direct messages for me
ParseQuery<Message> queryDirect = ParseQuery.getQuery(Message.class);
queryDirect.whereEqualTo("type", 0);
queryDirect.whereEqualTo("recipient", ParseUser.getCurrentUser());

//public announcements
ParseQuery<Message> queryAnnouncements = ParseQuery.getQuery(Message.class);
queryAnnouncements.whereEqualTo("type", 1);

//I want both direct and public
List<ParseQuery<Message>> queries = new ArrayList<ParseQuery<Message>>();
queries.add(queryDirect);
queries.add(queryAnnouncements);

ParseQuery<Message> queryMessages = ParseQuery.or(queries);
//... but only those which I haven't deleted for myself
queryMessages.whereNotEqualTo("deletedFor", ParseUser.getCurrentUser());
//puting them in correct order
queryMessages.addDescendingOrder("createdAt");
//and attaching the author ParseUser object (to get e.g. his name or URL to photo)
queryMessages.include("author");
queryMessages.findInBackground(new FindCallback<Message>() {/*DO SOMETHING HERE*/});

Solution

  • I would suggest changing your schema to better support public messages.

    You should have a single copy of the public message, as there's no changing the message itself.

    You should then store just the status for each user if it is anything other than "unread". This would be another table.

    When a MessageRequest comes in with type 1, create a new PublicMessage, don't create any status rows as everyone will use the default status of "unread". This makes your afterSave handler work cleanly as it is always creating just one new object, either a Message or a PublicMessage.

    As each user reads the message or deletes it, create new PublicMessageStatus row for that user with the correct status.

    When showing public messages to a user, you will do two queries:

    1. Query for PublicMessage, probably with some date range
    2. Query for PublicMessageStatus with a filter on user matching the current user and matchesQuery('publicMessage', publicMessageQuery) constraint using a clone of the first query

    Client side you'll then need to combine the two to hide/remove those with status "deleted" and mark those with status "read" accordingly.

    Update based on feedback

    You could choose instead to use a single Message class for public/private messages, and a MessageStatus class to handle status.

    Public vs Private would be based on the Message.recipient being empty or not.

    To get all messages for the current user:

    // JavaScript sample since you haven't specified a language
    // assumes Underscore library available
    
    var Message = Parse.Object.extend('Message');
    var MessageStatus = Parse.Object.extend('MessageStatus');
    
    var publicMessageQuery = new Parse.Query(Message);
    publicMessageQuery.doesNotExist('recipient');
    publicMessageQuery.notEqualTo('author', currentUser);
    
    var privateMessageQuery = new Parse.Query(Message);
    privateMessageQuery.equalTo('recipient', currentUser);
    
    var messagesQuery = new Parse.Query.or(publicMessageQuery, privateMessageQuery);
    messagesQuery.descending('createdAt');
    // set any other filters to apply to both queries
    
    var messages = [];
    messageQuery.find().then(function(results) {
        messages = _(results).map(function (message) {
            return { message: message, status: 'unread', messageId: message.objectId };
        });
        var statusQuery = new Parse.Query(MessageStatus);
        statusQuery.containedIn('message', results);
        statusQuery.equalTo('user', currentUser);
        // process status in order so last applies
        statusQuery.ascending('createdAt');
        return
    }).then(function(results) {
        _(results).each(function (messageStatus) {
            var messageId = messageStatus.get('message').objectId;
            _(messages).findWhere({ messageId: messageId }).status = messageStatus.get('status');
        });
    });
    
    // optionally filter messages that are deleted
    messages = _(messages).filter(function(message) { return message.status !== 'deleted'; });
    
    // feed messages array to UI...