Skip to content
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

Catch synchronous errors in Doc._otApply #259

Merged
merged 2 commits into from
Nov 28, 2018
Merged

Conversation

alecgibson
Copy link
Collaborator

Fixes #257

As outlined in the above issue, calling Doc.submitOp can sometimes
throw a synchronous error, for example when submitting an invalid op
that causes type.apply to throw.

This is surprising behaviour, because an error handler should already be
provided in the callback supplied to submitOp. What's more, the doc
could be left partially mutated and out-of-sync with the server, which
could lead to confusing behaviour.

This change wraps all internal usage of Doc._otApply in try/catch
blocks. If an error is thrown in this method, then we attempt to
recover with Doc._hardRollback, which will attempt to reset the
document and then call the callbacks of the pending ops with the error.

This change also prevents Doc._hardRollback from emitting an error if
at least one pending op callback was invoked, so that we don't branch
error handling behaviour.

@coveralls
Copy link

Coverage Status

Coverage decreased (-0.1%) to 95.392% when pulling 9484d7f on otapply-rollback into a67ad42 on master.

1 similar comment
@coveralls
Copy link

Coverage Status

Coverage decreased (-0.1%) to 95.392% when pulling 9484d7f on otapply-rollback into a67ad42 on master.

@coveralls
Copy link

coveralls commented Nov 15, 2018

Coverage Status

Coverage increased (+0.09%) to 95.602% when pulling 84a70af on otapply-rollback into a67ad42 on master.

@@ -889,7 +900,7 @@ Doc.prototype._hardRollback = function(err) {
this.fetch(function() {
var called = op && callEach(op.callbacks, err);
for (var i = 0; i < pending.length; i++) {
callEach(pending[i].callbacks, err);
called = callEach(pending[i].callbacks, err) || called;
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nateps @ericyhwang do you know if this change will have any weird side-effects? Originally it would not count a pending callback as having called something, so it would both call back, and emit, which seems a bit odd?

It's forced some updates to a couple of edge case tests, which I'll highlight also. I'm assuming they're okay to change...? In them, we actually assert that the callback is called and an error is emitted; is that the expected behaviour?

@alecgibson
Copy link
Collaborator Author

Also apologies for the code coverage decrease, but I don't have any idea how to reach the other catch blocks without either:

  • directly calling the internal methods (which feels like cheating)
  • abusing middleware to mutate doc.data after _submitOp, but before _handleOp (which also feels like cheating)

Fixes #257

As outlined in the above issue, calling `Doc.submitOp` can sometimes
`throw` a synchronous error, for example when submitting an invalid op
that causes `type.apply` to `throw`.

This is surprising behaviour, because an error handler should already be
provided in the callback supplied to `submitOp`. What's more, the doc
could be left partially mutated and out-of-sync with the server, which
could lead to confusing behaviour.

This change wraps all internal usage of `Doc._otApply` in `try`/`catch`
blocks. If an error is thrown in this method, then we attempt to
recover with `Doc._hardRollback`, which will attempt to reset the
document and then call the callbacks of the pending ops with the error.

This change also prevents `Doc._hardRollback` from emitting an error if
at least one pending op callback was invoked, so that we don't branch
error handling behaviour.
@alecgibson
Copy link
Collaborator Author

Okay - code coverage back up I think. I did actually manage to work out some crazy contrived (but theoretically possible) way that type.apply could throw after the op was successfully submitted.

@alecgibson
Copy link
Collaborator Author

As discussed, we should check that every op has a callback. If any of them don't, then we should emit an error (so that no errors are ever swallowed).

We previously followed the following logic for callbacks in a hard
rollback:

  - check there's an inflight op, and that it has a callback
  - if this isn't the case, then emit the error

This logic doesn't work very well in the situation where we don't have
an inflight op. This can happen in the following case:

  - I submit an invalid op
  - the op is added to the pending ops queue
  - we attempt to apply the op to the local document before flushing
  - `type.apply` throws, so the op is never flushed from pending to
    inflight
  - we perform a hard rollback
  - we now find we have no inflight op, but we do have a pending op

In this case, since we have no inflight op, we'd emit the error, but
also call the pending op callback, causing both a callback call _and_
an error emission, which is a bit surprising.

This change updates our logic, so that:

  - we check that there are some ops to callback
  - and that all of those ops have callbacks

If there are no callbacks, or if any of the ops don't handle the error,
then we emit, so that we can be sure of never swallowing an error. We
also avoid treating the inflight op any differently to pending ops, so
that we avoid behaviour that's dependent on where in its lifecycle an op
throws.

However, if the client handles all of their op submission errors, then
we never emit.
@alecgibson
Copy link
Collaborator Author

@ericyhwang @nateps I've updated the hard rollback callback logic. I've actually tweaked it a tiny bit more than discussed - the inflight op is no longer treated as "special" relative to other pending ops. Here's the commit message for clarity:

We previously followed the following logic for callbacks in a hard
rollback:

  • check there's an inflight op, and that it has a callback
  • if this isn't the case, then emit the error

This logic doesn't work very well in the situation where we don't have
an inflight op. This can happen in the following case:

  • I submit an invalid op
  • the op is added to the pending ops queue
  • we attempt to apply the op to the local document before flushing
  • type.apply throws, so the op is never flushed from pending to
    inflight
  • we perform a hard rollback
  • we now find we have no inflight op, but we do have a pending op

In this case, since we have no inflight op, we'd emit the error, but
also call the pending op callback, causing both a callback call and
an error emission, which is a bit surprising.

This change updates our logic, so that:

  • we check that there are some ops to callback
  • and that all of those ops have callbacks

If there are no callbacks, or if any of the ops don't handle the error,
then we emit, so that we can be sure of never swallowing an error. We
also avoid treating the inflight op any differently to pending ops, so
that we avoid behaviour that's dependent on where in its lifecycle an op
throws.

However, if the client handles all of their op submission errors, then
we never emit.

Copy link
Contributor

@ericyhwang ericyhwang left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, and LGT-Nate as well. Thanks for finding and fixing this case!

@ericyhwang ericyhwang merged commit 70b8815 into master Nov 28, 2018
@ericyhwang ericyhwang deleted the otapply-rollback branch November 28, 2018 17:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants