Hi Jan,
Following the implementation of your advice, I continued to see
inconsistent data. I mean your advice has probably helped on some updates
but not all.
So I continued to dig into the issue and finally got to the point where I
may have found some problem with 'byExample' (which is used to cache those
stats during the transaction).
Here is a snippet reproducing the behavior:
controller.post('/foxxdebug', function (req, res) {
var from = 'foxxdebug/' + makeid(32); // creates some random ID
var to = 'foxxdebug/' + makeid(32);
db._executeTransaction({
collections: {
read: ['foxxdebug'],
write: ['foxxdebug']
},
action: function () {
var newDoc = db.foxxdebug.insert(from, to, { request: true });
var fromCount1 = db.foxxdebug.byExample({ _from: from, request:
false }).count();
var toCount1 = db.foxxdebug.byExample({ _to: to, request: false
}).count();
newDoc.request = false;
db.foxxdebug.replace({ _id: newDoc._id }, newDoc);
var fromCount2 = db.foxxdebug.byExample({ _from: from, request:
false }).count();
var toCount2 = db.foxxdebug.byExample({ _to: to, request: false
}).count();
res.json({ fromCount1: fromCount1, toCount1: toCount1,
fromCount2: fromCount2, toCount2: toCount2 });
}
});
});
I would expect the response to be:
{ "fromCount1": 0, "toCount1": 0, "fromCount2": 1, "toCount2": 1 }
but I get
{ "fromCount1": 1, "toCount1": 1, "fromCount2": 1, "toCount2": 1 }
and I'm pretty sure that's related to the issue I'm seeing.
Any further help would be greatly appreciated, thanks!
On Tuesday, June 14, 2016 at 11:32:37 PM UTC+8, Jan Steemann wrote:
>
> Hi Thomas,
>
> yes, I think that should also fix it.
> `post.stats = ...` will set the `stats` property of the `post` object,
> which is copy-on-write and thus fine.
> Only accessing a sub-property of a property will cause issues when the
> document is a ShapedJson instance.
>
> Best regards
> Jan
>
> 2016-06-14 15:27 GMT+02:00 Thomas Weiss <[email protected]
> <javascript:>>:
>
>> Hi Jan,
>>
>> Thank you so much for this very extensive reply. I know you guys are
>> working hard on the 3.0 release so I really appreciate the time you took to
>> write this answer!
>>
>> If I understand correctly what you explained, am I right that the
>> following code would also solve my issue:
>> post.stats = {
>> likeCount: db.likes.byExample({ _to: 'posts/' + postId }).count(),
>> commentCount: post.stats.commentCount,
>> shareCount: post.stats.shareCount
>> };
>> db.posts.replace({ _id: post._id }, post);
>>
>> Cheers,
>> Thomas
>>
>> On Tuesday, June 14, 2016 at 3:59:57 PM UTC+8, Jan Steemann wrote:
>>>
>>> Hi there,
>>>
>>> I think I can shed some light on this. I don't think this is related to
>>> waitForSync or transaction, but has a different root cause.
>>>
>>> All documents, when inserted, updated or replaced are saved in
>>> ArangoDB's write-ahead log (WAL) first.
>>> When retrieving a document from the WAL using
>>> db.<collection>.document(key) then a copy of the document will be returned
>>> to Foxx/JavaScript as a regular JavaScript object:
>>>
>>> /* fetch document with key postId from database. will return a
>>> JavaScript object */
>>> db.posts.insert({ _key: postId, stats: { likeCount: 0 } });
>>> var post = db.posts.document(postId);
>>>
>>> This object can be modified regularly and saved again, using
>>> update/replace:
>>>
>>> post.stats.likeCount = db.likes.byExample({ _to: 'posts/' + postId
>>> }).count();
>>> db.posts.replace({ _id: post._id }, post);
>>>
>>> However, at some point the documents are moved from the WAL into the
>>> datafiles of the respective collections.
>>> When they are then retrieved from the datafiles, they are returned as
>>> light-weight JavaScript objects which contain only pointers to the
>>> underlying document data.
>>> Accessing a property of such light-weight object will trigger a property
>>> handler, which will build the result value and return it as a *temporary
>>> object*
>>> That means when accessing `post.stats` in the following code,
>>> `post.stats` will return a temporary object, for which the property
>>> `likeCount` will be updated:
>>>
>>> /* returns light-weight object with property handlers */
>>> var post = db.posts.document(postId);
>>> /* post.stats will produce a tempory object, and
>>> post.state.likeCount will access a property of the temporary object */
>>> post.stats.likeCount = db.likes.byExample({ _to: 'posts/' + postId
>>> }).count();
>>>
>>> The modification will only happen in the temporary object, but not in
>>> the `post` object.
>>> When then the post object is written back to the database, it won't
>>> contain the changes.
>>>
>>> Following is some example code that demonstrates this for a document
>>> that is stored in the WAL and for a document from the collection datafiles.
>>> In one case the update/replace will do what's expected and in the other
>>> it won't:
>>>
>>> ```
>>> db._drop("posts");
>>> db._drop("likes");
>>>
>>> db._create("posts");
>>> db._createEdgeCollection("likes");
>>>
>>> var ShapedJson = require("internal").ShapedJson;
>>>
>>> function updateCount(issuerId, postId) {
>>> var post = db.posts.document(postId);
>>> db.likes.insert('users/' + issuerId, 'posts/' + postId, {}, true);
>>> // the post object was fetched before
>>> post.stats.likeCount = db.likes.byExample({ _to: 'posts/' + postId
>>> }).count();
>>> db.posts.replace({ _id: post._id }, post);
>>> }
>>>
>>> // insert a new post document. it will be located in the write-ahead log
>>> first
>>> db.posts.insert({ _key: "post1", stats: { likeCount: 0 } });
>>> require("internal").print("the document:", db.posts.document("post1"));
>>> require("internal").print("is ShapedJson:", db.posts.document("post1")
>>> instanceof ShapedJson);
>>>
>>> // update the document's count. this will work fine
>>> updateCount("test1", "post1");
>>>
>>> // and print results. count should have been updated
>>> require("internal").print("count for post1 after update is:",
>>> db.posts.document("post1"));
>>>
>>>
>>>
>>> // insert another post document. it will be located in the write-ahead
>>> log first
>>> db.posts.insert({ _key: "post2", stats: { likeCount: 0 } });
>>> // now flush the write-ahead log manually and wait for a few seconds
>>> // note: flushing the write-ahead log may happen automatically from time
>>> to time while
>>> // the server is running
>>> require("internal").wal.flush(true, true);
>>> require("internal").wait(5, false);
>>>
>>> require("internal").print("the document:", db.posts.document("post2"));
>>> require("internal").print("is ShapedJson:", db.posts.document("post2")
>>> instanceof ShapedJson);
>>>
>>> // now update the document's count. this will update the count value of
>>> a ShapedJson
>>> // (which is a temporary object when retrieved from the database)
>>> updateCount("test2", "post2");
>>>
>>> // and print results. count will not have been updated
>>> require("internal").print("count for post2 is now:",
>>> db.posts.document("post2"));
>>> ```
>>>
>>> Note that in the above example for both documents there is a check
>>> whether they are an instance of *ShapedJson*.
>>> In the one case (WAL case) the document won't be a ShapedJson instance
>>> but a regular JS object, but in the other case it will be.
>>> In the latter case, updating a property of the document which itself is
>>> an array or object won't work, because of the temporaries produced by the
>>> property handler.
>>>
>>> What helps in this case is to convert the ShapedJson instance of the
>>> document into a regular JS object, e.g. via
>>>
>>> var _ = require("underscore");
>>> ...
>>> var post = _.clone(db.posts.document(postId));
>>>
>>> Or, combined with the ShapedJson test:
>>>
>>> var ShapedJson = require("internal").ShapedJson;
>>> var _ = require("underscore");
>>> ...
>>> var post = db.posts.document(postId);
>>>
>>>
>>> if (post instance of ShapedJson) {
>>> // ShapedJson object
>>> post = _.clone(post);
>>> }
>>>
>>> Note that this applies to the 1.x and 2.x branches of ArangoDB.
>>> We have simplified this a lot in 3.0 so both the ShapedJson testing and
>>> cloning can be omitted there. All objects returned in 3.0 will be regular
>>> JS objects free of side effects.
>>>
>>> I hope this helps.
>>> Best regards
>>> Jan
>>>
>>>
>>>
>>> 2016-06-14 5:18 GMT+02:00 Thomas Weiss <[email protected]>:
>>>
>>>> As a follow-up, I've just seen the same behavior on my dev machine, it
>>>> just happens less frequently (probably because it has better perfs than
>>>> the
>>>> server used for staging).
>>>> Any comment will be welcome!
>>>>
>>>> Thanks,
>>>> Thomas
>>>>
>>>>
>>>> On Sunday, June 12, 2016 at 3:57:04 PM UTC+8, Thomas Weiss wrote:
>>>>>
>>>>> Hi there,
>>>>>
>>>>> I'm using a Foxx app to execute transactions. To boost query
>>>>> efficiency (the system is read-heavy), I try to cache the count of edges
>>>>> directly in the docs, and do this like that:
>>>>> db.likes.insert('users/' + issuerId, 'posts/' + postId, {}, true);
>>>>> // the post object was fetched before
>>>>> post.stats.likeCount = db.likes.byExample({ _to: 'posts/' + postId }).
>>>>> count();
>>>>> db.posts.replace({ _id: post._id }, post);
>>>>> Notice the last *true* parameter passed to *insert* to make sure that
>>>>> we wait for the write to be flushed. This code is executed within a
>>>>> transaction by the way.
>>>>>
>>>>> This works well on my dev machine (Windows 10) *but* it seems that
>>>>> the count is not systematically updated correctly on the staging server
>>>>> (Ubuntu). What's weird is that sometimes it works and sometimes it
>>>>> doesn't,
>>>>> which makes me think about a threading/concurrency issue (and that's why
>>>>> I
>>>>> added the waitForSync param).
>>>>>
>>>>> Any help or suggestion would be greatly appreciated here.
>>>>>
>>>>> Thanks,
>>>>> Thomas
>>>>>
>>>> --
>>>> You received this message because you are subscribed to the Google
>>>> Groups "ArangoDB" group.
>>>> To unsubscribe from this group and stop receiving emails from it, send
>>>> an email to [email protected].
>>>> For more options, visit https://groups.google.com/d/optout.
>>>>
>>>
>>> --
>> You received this message because you are subscribed to the Google Groups
>> "ArangoDB" group.
>> To unsubscribe from this group and stop receiving emails from it, send an
>> email to [email protected] <javascript:>.
>> For more options, visit https://groups.google.com/d/optout.
>>
>
>
--
You received this message because you are subscribed to the Google Groups
"ArangoDB" group.
To unsubscribe from this group and stop receiving emails from it, send an email
to [email protected].
For more options, visit https://groups.google.com/d/optout.