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

⚡️ Avoid committing no-ops to the database #682

Merged
merged 1 commit into from
Oct 15, 2024

Conversation

alecgibson
Copy link
Collaborator

@alecgibson alecgibson commented Oct 3, 2024

This change checks for no-ops just before committing, and avoids writing them to the database.

Motivation

Sometimes we have situations where multiple clients try to make an identical change to the same document at the same time (if multiple clients are trying to keep two documents in sync, for example).

When this happens, this can result in a O(n^2) retry cascade, since the requests will all keep retrying, with only a single request succeeding at a time, even if those ops are all no-ops.

Solution

This change adds a new special ERR_NO_OP error code. If the server detects a no-op, it will skip committing, and early-return this error code, which takes the request out of the retry queue.

The client then has some special handling for this case, where it will:

  1. Fetch from remote to get up-to-date with any potential conflicting ops
  2. Then acknowledge the in-flight op, without bumping the version (since the op wasn't committed)

Note that if $fixup() has been used, we do not early-return a no-op error, even if the fixup results in a no-op, since the client would be left in an inconsistent state without the fixup ops being returned and applied.

Depends on:

@alecgibson alecgibson force-pushed the avoid-committing-no-ops branch 2 times, most recently from 1466e83 to 714811b Compare October 3, 2024 12:08
@coveralls
Copy link

coveralls commented Oct 3, 2024

Coverage Status

coverage: 97.487% (+0.006%) from 97.481%
when pulling f91d42c on avoid-committing-no-ops
into 6a2c194 on master.

// The op is a no-op, either because it was submitted as such, or - more
// likely - because it was transformed into one. Let's avoid committing it
// and tell the client.
return callback(request.noOpError());
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This check has to happen at some point after the op is transformed, and before the op is committed.

I don't think I have a strong opinion on when we return this, so long as it's at either one of these extremes(?).

The benefit of doing it just before commit() is that it's a bit more directly linked to the fact that commit is about to be skipped. It does mean that all the middleware will be fired, though (which may or may not be a good/expected thing?).

Copy link
Contributor

Choose a reason for hiding this comment

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

After some discussion, we decided to move just after transform, before the "apply" hook.

The "commit" hook right now means that sharedb core is going to call the DB adapter's commit method, and leaving the no-op early return here would have meant a violation of that behavior. By moving it earlier, that behavior is preserved.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Should this be an opt-in feature behind a flag? I don't know if this should be considered a breaking change?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Okay I wound up:

  • hiding this behind a feature flag
  • bumping the protocol to 1.2 and adding a protocol check, so that old clients won't get ERR_NO_OP errors that they don't know how to handle

@@ -35,6 +35,7 @@ ShareDBError.CODES = {
ERR_MAX_SUBMIT_RETRIES_EXCEEDED: 'ERR_MAX_SUBMIT_RETRIES_EXCEEDED',
ERR_MESSAGE_BADLY_FORMED: 'ERR_MESSAGE_BADLY_FORMED',
ERR_MILESTONE_ARGUMENT_INVALID: 'ERR_MILESTONE_ARGUMENT_INVALID',
ERR_NO_OP: 'ERR_NO_OP',
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we add docs about the error?

@alecgibson alecgibson force-pushed the avoid-committing-no-ops branch 2 times, most recently from 6e08880 to bab5db1 Compare October 7, 2024 11:57
@alecgibson alecgibson marked this pull request as draft October 7, 2024 11:58
@alecgibson alecgibson force-pushed the avoid-committing-no-ops branch 2 times, most recently from 179a883 to 3eec5a0 Compare October 7, 2024 12:07
@alecgibson alecgibson changed the base branch from master to client-protocol October 7, 2024 12:58
Base automatically changed from client-protocol to master October 15, 2024 16:27
@alecgibson alecgibson marked this pull request as ready for review October 15, 2024 16:27
@alecgibson alecgibson force-pushed the avoid-committing-no-ops branch 5 times, most recently from 5157251 to d7a0921 Compare October 15, 2024 16:53
This change checks for no-ops just before committing, and avoids writing
them to the database.

Motivation
----------
Sometimes we have situations where multiple clients try to make an
identical change to the same document at the same time (if multiple
clients are trying to keep two documents in sync, for example).

When this happens, this can result in a `O(n^2)` retry cascade, since
the requests will all keep retrying, with only a single request
succeeding at a time, even if those ops are all no-ops.

Solution
--------
This change adds a new special `ERR_NO_OP` error code. If the server
detects a no-op, it will skip committing, and early-return this error
code, which takes the request out of the retry queue.

The client then has some special handling for this case, where it will:

 1. Fetch from remote to get up-to-date with any potential conflicting
    ops
 2. Then acknowledge the in-flight op, **without** bumping the version
    (since the op wasn't committed)

Note that if `$fixup()` has been used, we **do not** early-return a
no-op error, even if the fixup results in a no-op, since the client
would be left in an inconsistent state without the fixup ops being
returned and applied.
@alecgibson alecgibson merged commit 50b40bb into master Oct 15, 2024
9 checks passed
@alecgibson alecgibson deleted the avoid-committing-no-ops branch October 15, 2024 17:00
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.

4 participants