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

Situation
I have a messaging Android app providing following feature(s)
to send message direct message to one selected recipient
to create public announcement that all users using the app receive (except author)
each user sees on his phone a list of messages he got
each message is either unread, read, or deleted
I use Parse.com as back-end
Current implementation
On Android client
When there is a new message, a new messageRequest of the MessageRequest class is created
If the message should be direct, the messageRequest has type 0, otherwise type 1
If the message is direct, there is recipient stored in the messageRequest object
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 case of direct message - one new message object of the Message class is created and saved
in case of public announcement - for each user except author, a new message object is created and added to a list of messages, then the list is saved
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
My only other idea how to solve this is to have only one Message object, and two columns called e.g. viewedBy and deletedFor which would contain lists of users that already viewed the message or have delete the message for them.
In this case, I'm not very sure about the performance of the queries
Also, I know, many of you think Why isn't he using table for splitting the M:N relation between the MessageRequest(which could be actually called Message in that case) and User?
My answer is that I had this solution, but it was harder to work with it in the Android code, more pointers, more includes in queries, etc.
Moreover, I would have to create the same amount of objects representing status for each user in the on parse.com back-end anyway, so I think the problem with Execution time out would be the same in the end
Update - mockup representing user's "Inbox"
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
I have just one Message object, via type I identify if it is direct or public
Two array columns were added
viewedBy - containing users that already viewed the message
deletedFor - containing users that marked the message as deleted for them
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*/});

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:
Query for PublicMessage, probably with some date range
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...

Related

Graph Subscriptions Beta - AdditionalData

in the Graph Beta Subscriptions API, there is a property that can be set or got on the Subscription object called AdditionalData.
I am trying to use this when creating a subscription to transport data that will be sent back with change notifications and provide more context to my task.
I am finding though that even though I set the property, it does not keep my added dictionary items but replaces with its own additional data.
Not sure if I am using this property for something that I shouldn't be or whether this is a bug or am I just setting it wrong? I am doing something like this:
var subscription = new Subscription
{
Resource = $"users/{userObjectId}/mailFolders('{resource}')/messages",
ChangeType = "created",
NotificationUrl = notificationWebHookUrl,
LifecycleNotificationUrl = lifecycleNotificationWebHookUrl,
AdditionalData = new Dictionary<string, object> { { "test", "123"} },
ExpirationDateTime = DateTime.UtcNow + new TimeSpan(0, 0, 4200, 0)
};
The subscription object doesn't support storing additional data. You can have custom data passed back to your notification URL when notifications are being delivered two ways:
You can include some of that data in the clientState, although this property is designed to be used as a security feature, you can put anything you want in there.
Any query string parameter included in the notification URL will be passed back when notifications are being delivered. I just authored a pull request to add that missing information.

Multiple Immutable Id's for same mail message?

I am writing an Office Outlook add-in that has a React frontend and a dotnet core backend. I have set up a subscription using the Graph API to receive notifications when a new email appears on the SentItems folder. I want to correlate the email from the notification with information I have stored in a database.
Unfortunately the item id changes when the email is sent and moves from the Drafts folder into SentItems so it isn't useful for matching.
There is a new ImmutableId that doesn't change when the email is moved between folders. I've been unable to get the Office.js lib to generate an ImmutableId but there is a translateExchangeIds method that when given an email item id will return an immutable id.
// convert to immutable
var translateRequest = new {
inputIds = new string [] { mailMessage.ItemId },
targetIdType = "restImmutableEntryId",
sourceIdType = "restId"
};
var immutableResponse = await graphClient.PostAsJsonAsync("me/translateExchangeIds", translateRequest);
var immutableId = await immutableResponse.Content.ReadAsStringAsync();
I can use that immutable id to retrieve the email message using the Graph request:
await graphClient.GetAsync($"Users/cccccccc-dddd-eeee-ffff-ba0c52e56d99/Messages/AAkALgAAAAAAHYQDEapmEc2byACqAC-EWg0AQ-irLc2NFESKcGAhz1k_GBBDB5JMOwAA/
But the immutable id that is returned with the subscription notification is a different immutable id for the same message. So it's not possible to match the notification mail message with the message info stored in my database. So I still have to attach a custom property to the message for the sole purpose of matching the database entry with the SentItems notification.
Is there a better way to deal with this issue?
Update: my theory is the difference occurs because the immutable id is derived when the item is in different folders? When translating the item id to an immutable id, the item is still in the Drafts folder. When the subscription notification occurs, the item is in the Sent Items folder. The following responses were from queries using the different immutable id's but identify the same message - the myId GUID is a custom property attached to the message and used to correlate the notification with the message info stored in a local database.
\"id\":\"AAkALgAAAAAAHYQDEapmEc2byACqAC-EWg0AQ-irLc2NFESKcGAhz1k_GAADB4INPAAA\",...,\"myId\":\"8baa904f-cf64-437c-878c-be4f71714aee\"
\"id\":\"AAkALgAAAAAAHYQDEapmEc2byACqAC-EWg0AQ-irLc2NFESKcGAhz1k_GAADB4INLwAA\",...,\"myId\":\"8baa904f-cf64-437c-878c-be4f71714aee\"
We fixed this and now you should see the same ImmutableId for draft and sent messages. Can you try and let us know if this is working as expected?

How to show data changes locally before it updated and showed up in firestore

I'm following a tutorial for an chat app with flutter and firebase firestore. In that app when user sends a text message to other it's not visible to sender till it's get updated in firestore and then the new data is showing up in the messages list. I want to is it possible to add the newly sent message to the messages list with a clock image indicating that it will update in database and then after it's updated in database then locally added message should be replaced with data from firestore in messages list.
Here is snippet of button press to send message...
void onSendMessage(String content, int type) {
// type: 0 = text, 1 = image, 2 = sticker
if (content.trim() != '') {
textEditingController.clear();
var documentReference = Firestore.instance
.collection('messages')
.document(groupChatId)
.collection('message')
.document(DateTime.now().millisecondsSinceEpoch.toString());
Firestore.instance.runTransaction((transaction) async {
await transaction.set(
documentReference,
{
'idFrom': id,
'idTo': peerId,
'timestamp': DateTime.now().millisecondsSinceEpoch.toString(),
'content': content,
'type': type
},
);
});
listScrollController.animateTo(0.0,
duration: Duration(milliseconds: 300), curve: Curves.easeOut);
} else {
Fluttertoast.showToast(msg: 'Nothing to send');
}
}
and here is the link to the tutorial Chat app with flutter and firestore
This shouldn't be too difficult to achieve.
Currently any new chat message is sent to the Firestore database upon submission.
You just need to extend the onSubmit function that does it (it probably has another name in your code example) to also add the message temporarily to the local message list.
Once you receive the update from the database you just have to remove the temporary message from the list since it will be replaced with the contents from the database. Include the progress indicator with the temporary message and you're done.
If you need a more detailled answer please add some code to your question (ideally together with a link to the original tutorial. Note: When providing a link to an external page make sure that you question is still complete should the link go dead.)
I used a cloud function to achieve this. I have a field isSent on my message and it's initially false, when the message gets created in firestore, I then update the isSent field using the cloud function and that automatically rebuilds the UI. It's not very fast and accurate, but much better than none

Parse Cloud Code GeoPoint

I have a Parse App which has two basic objects. The first object "User" has a property called "location" which stores a GeoPoint.
The second object is "Sighting" which has a "location" property with a GeoPoint.
In my cloud code I have sucessfuly introduced an aftersave function on the "Sighting" object so that a push notification is sent to everybody once a sighting has been saved.
However what I am trying to achieve is only sending those users within a certain range.
I have found query.withinKilometeres but I cannot understand how to compare all User."Locations" and take the saved object "Location" as the base. Thereafter send all returned users a push.
The cloud code is totally alien to me so any help is appreciated.
Thanks
James
The target of push is _Installation.
In your question, you should maintain a pointer 'user' for related currentUser of the device.
//afterSave of Sighting
var sighting = request.object;
//means created, not update
if(!sight.existed()){
var location = sighting.get('location');
var userQuery = new Parse.Query(Parse.User);
//1 for example
userQuery.withinKilometeres('location', 1);
var query = new Parse.Query(Parse.Installation);
query.matchesQuery('user', userQuery);
Parse.Push.send({
where:query,
data:{
alert: "this is msg",
title: "this is title"
}
},{useMasterKey:true})
.then(function(){
console.log('push done');
}, function(error){
console.error(error);
});
}
There are a limitation of matchesQuery. In this case, you cannot send more than 1000 user (to get related installations).
So I suggest you put location info in _Installation, and it more make sense(Your user may have multiple device, but User cannot split to 2 places)

How to send and receive domain objects with rabbitMQ plugin and grails

I've went over the excellent documentation for the rabbitMQ plugin. However, I am still confused about a few things.
Scenario
My application will take a file upload from the user, do various things to the file and accordingly set certain properties on the domain object. Some of this work can be labor intensive so I am using a queue. I envision requests being queued and the consumer picking up the requests from the queue and consuming them.
Questions
I want to store a domain object in the queue. I do this by: rabbitSend 'myqueue', colorObj. colorObjis an object of domain class Color
However, in the ColorService handleMessage(...) When I fetch the item from the queue, the item is not of type Color. Please note that on the rabbitMQ dashboard I can see the items being inserted in the queue, so my queue initiation in config.groovy is fine (I am using amq.direct)
How can I send and fetch a domain object from the queue?
From the behavior I've seen thus far, the handleMessage does not need to be instantiated. Even if I don't call the ColorService it still executes handleMessage by itself. Is that a normal behavior?
Below is the code:
controller
Color colorObj = colorService.newRequest(params, request.getFile('color.filename')
if (colorObj.validate)
rabbitSend 'myqueue', colorObj
...
service
class ColorService {
static rabbitQueue = 'myqueue'
void handleMessage(message) {
println "came in message: " + message instanceof Color //this prints false
}
}
As Tim says, if you can get by with just passing the domain instance ID that is simplest. You do need to be careful of changes to the domain instance while the message is in transit though.
Alternatively, if it's the data you're interested in, I prefer to serialise the objects as JSON using something like
rabbitSend 'myqueue', (colorObj as JSON).toString()
Of course, now your listener is receiving a string, so you'll have to convert it back:
void handleMessage(String message) {
def color = new Color(JSON.parse(message))
println "came in message: " + color instanceof Color
}
There is a little bit of discussion around this on the GPRABBITMQ-15 issue.
As it shows in the documentation, you can either send a String or a Map
Why not send the id of your domain object:
rabbitSend 'myqueue', colorObj.id
Then, load it back in when the message is processed:
void handleMessage(message) {
println "Got ${Color.get( message )}"
}
Or, if you don't need the domain object until the message is processed, send a map of all the required data, and have the service create the domain object after it is processed successfully?

Resources