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

🐛 Unhandled rejection when submitting ops during hard rollback. #692

Merged
merged 2 commits into from
Mar 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 17 additions & 4 deletions lib/client/doc.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ function Doc(connection, collection, id) {
this.pendingFetch = [];
this.pendingSubscribe = [];

this._isInHardRollback = false;

// Whether we think we are subscribed on the server. Synchronously set to
// false on calls to unsubscribe and disconnect. Should never be true when
// this.wantSubscribe is false
Expand Down Expand Up @@ -748,10 +750,18 @@ Doc.prototype._submit = function(op, source, callback) {
// The op contains either op, create, delete, or none of the above (a no-op).
if ('op' in op) {
if (!this.type) {
var err = new ShareDBError(
ERROR_CODE.ERR_DOC_DOES_NOT_EXIST,
'Cannot submit op. Document has not been created. ' + this.collection + '.' + this.id
);
if (this._isInHardRollback) {
var err = new ShareDBError(
ERROR_CODE.ERR_DOC_IN_HARD_ROLLBACK,
'Cannot submit op. Document is performing hard rollback. ' + this.collection + '.' + this.id
);
Comment on lines +753 to +757
Copy link
Collaborator

Choose a reason for hiding this comment

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

You could just move this after the var err = new ShareDBError(ERR_DOC_DOES_NOT_EXIST) to override this "default" error and then avoid having to duplicate the emit() machinery

} else {
var err = new ShareDBError(
ERROR_CODE.ERR_DOC_DOES_NOT_EXIST,
'Cannot submit op. Document has not been created. ' + this.collection + '.' + this.id
);
}

if (callback) return callback(err);
return this.emit('error', err);
}
Expand Down Expand Up @@ -1030,6 +1040,7 @@ Doc.prototype._rollback = function(err) {
};

Doc.prototype._hardRollback = function(err) {
this._isInHardRollback = true;
// Store pending ops so that we can notify their callbacks of the error.
// We combine the inflight op and the pending ops, because it's possible
// to hit a condition where we have no inflight op, but we do have pending
Expand All @@ -1047,6 +1058,8 @@ Doc.prototype._hardRollback = function(err) {
// Fetch the latest version from the server to get us back into a working state
var doc = this;
this._fetch({force: true}, function(fetchError) {
doc._isInHardRollback = false;

// We want to check that no errors are swallowed, so we check that:
// - there are callbacks to call, and
// - that every single pending op called a callback
Expand Down
1 change: 1 addition & 0 deletions lib/error.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ ShareDBError.CODES = {
ERR_DOC_DOES_NOT_EXIST: 'ERR_DOC_DOES_NOT_EXIST',
ERR_DOC_TYPE_NOT_RECOGNIZED: 'ERR_DOC_TYPE_NOT_RECOGNIZED',
ERR_DOC_WAS_DELETED: 'ERR_DOC_WAS_DELETED',
ERR_DOC_IN_HARD_ROLLBACK: 'ERR_DOC_IN_HARD_ROLLBACK',
ERR_INFLIGHT_OP_MISSING: 'ERR_INFLIGHT_OP_MISSING',
ERR_INGESTED_SNAPSHOT_HAS_NO_VERSION: 'ERR_INGESTED_SNAPSHOT_HAS_NO_VERSION',
ERR_MAX_SUBMIT_RETRIES_EXCEEDED: 'ERR_MAX_SUBMIT_RETRIES_EXCEEDED',
Expand Down
19 changes: 19 additions & 0 deletions test/client/doc.js
Original file line number Diff line number Diff line change
Expand Up @@ -636,6 +636,25 @@ describe('Doc', function() {
}
], done);
});

it('rejects ops with ERR_DOC_IN_HARD_ROLLBACK error when in hard rollback', function(done) {
var backend = this.backend;
var doc = backend.connect().get('dogs', 'snoopy');

async.series([
doc.create.bind(doc, {color: 'red'}),
function(next) {
doc.on('error', function(error) {
expect(error.code).to.be.equal('TEST_ERROR');
});
doc._hardRollback(new ShareDBError('TEST_ERROR'));
doc.submitOp({p: ['color'], oi: 'blue', od: 'red'}, function(error) {
expect(error.code).to.be.equal('ERR_DOC_IN_HARD_ROLLBACK');
next();
});
}
], done);
});
});

describe('errors on ops that could cause prototype corruption', function() {
Expand Down