-
Notifications
You must be signed in to change notification settings - Fork 452
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix Doc.prototype.destroy #204
Conversation
The problem was that unsubscribe re-added the doc to the connection. Now the doc is removed from the connection after unsubscribe. Additionally, we're no longer waiting for the unsubscribe response before executing the callback. It is consistent with Query, unsubscribe can't fail anyway and the subscribed state is updated synchronously on the client side.
lib/client/doc.js
Outdated
@@ -104,10 +104,8 @@ emitter.mixin(Doc); | |||
Doc.prototype.destroy = function(callback) { | |||
var doc = this; | |||
doc.whenNothingPending(function() { | |||
if (doc.wantSubscribe) doc.unsubscribe(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMO the curly brace style is more clear, but it's a rather pedantic point.
Any way to add tests for this?
Thanks for the review @curran. I added the braces as you suggested - I also prefer this style but tried to be consistent with the rest of the code base, which very often omits braces. I also added an assertion to an existing test which failed before my changes and now it passes. |
@nateps @ericyhwang , could you provide some feedback on this PR, or merge it, if it's all good, please? |
We've got our ~monthly Share PR review meeting tomorrow, so we'll take a look at this and the other couple PRs during the meeting! (I'll also ask about perhaps switching to shorter, more frequent review sessions, since that'll be easier all around if Nate's schedule can now accommodate it.) |
Great catch! I agree that it isn't 100% clear whether we need to wait for the unsubscribe to happen, and we might get away with calling the callback before the unsubscribe is fully effective. As you mentioned, the client calls back when it is disconnected. Here is the reasoning behind that: Calling back on disconnect is needed to ensure that the callback to unsubscribe is always called. (The unsubscribe callback may be called after acknowledgement from the sever if we are connected, in a nextTick if we are disconnected, or at the time of disconnection.) If the client is disconnected, the server won't be able to send response messages and it will clean up the server-side agent responsible for the client. The client will get a new agent if it reconnects. So from the perspective of the client, the unsubscribe is effective immediately after a disconnection. It is different if the client is connected, since if we call Roughly, I could imagine something like the following being an issue:
(This would be an issue with existing ShareDB code as well. Just clarifying why I think we should wait until unsubscribe calls back.) The above is really complicated, but I think it might be an issue, and there is an easy way to avoid testing fate in this case. Knowing that unsubscribe will always call back and destroy waits until pending operations are complete, I think it is best if we just wait until unsubscribe calls back in all cases before calling the destroy callback. I'm a lot more confident we won't run into any race conditions if we do it that way. Thus, I recommend the following: Doc.prototype.destroy = function(callback) {
var doc = this;
doc.whenNothingPending(function() {
if (doc.wantSubscribe) {
doc.unsubscribe(function() {
doc.connection._destroyDoc(doc);
callback();
});
} else {
doc.connection._destroyDoc(doc);
if (callback) callback();
}
});
}; |
I think it's ok to wait for Waiting for unsubscribeFirst of all, here's a slightly improved version of Doc.prototype.destroy = function(callback) {
var doc = this;
var sync = true; // indicates if whenNothingPending's callback was executed synchronously
doc.whenNothingPending(function() {
if (doc.wantSubscribe) {
doc.unsubscribe(function(err) {
if (!err) doc.connection._destroyDoc(doc);
if (callback) return callback(err);
if (err) this.emit('error', err);
});
} else {
doc.connection._destroyDoc(doc);
if (callback) {
if (sync) process.nextTick(callback);
else callback();
}
}
});
sync = false;
}; Why I think it's not necessaryThe scenario you outlined above looks like one OT is designed to handle. From reading the source code, it looks like ShareDB can handle ops coming in wrong order, targeted at an older or newer version of the snapshot, with conflicting version, duplicated, etc. So, receiving any extraneous or conflicting operations, or missing some operations, is not a problem. It also looks like extraneous calls to Bigger problemDocs are shared freely between queries and can be retrieved individually. This is great for performance and efficient, however, it might lead to unpredictable behaviour. For example, an application may have several independent components using ShareDB. If any of the components calls Here's a simple scenario in which
Components A and B now have 2 separate Doc instances but Connection can support only one. As the components use their Docs, they'll surely get some incorrect behaviour. I'm not sure what's the best way to solve it... perhaps ref counting to know when Doc is no longer used and can be safely removed from Connection. For subscriptions we could increment |
Nate's comments from the PR review meeting:
|
I made the requested changes and created a new PR to fix whenNothingPending. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the contribution! Definitely good fix to make sure we are cleaning up memory properly. 💥
doc.unsubscribe(function(err) { | ||
if (err) { | ||
if (callback) callback(err); | ||
else this.emit('error', err); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this
should be doc. I'll just go ahead and merge this change and make the fix, since we're close.
It fixes the issue where a document is re-added to a collection after calling
destroy
, causing a memory leak.It should also fix #161.
Re not waiting for the unsubscribe callback in
destroy
, I think it is not necessary because: