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

feat: add support for optimistic transactions #1548

Open
wants to merge 7 commits into
base: main
Choose a base branch
from

Conversation

schmidt-sebastian
Copy link
Contributor

@schmidt-sebastian schmidt-sebastian commented Jun 29, 2021

This PR adds support for optimistic transactions. A verify write is added to the commit for all documents read during an optimistic transaction.

This PR relies on an backend feature that is not yet public. We need to figure out what our strategy here will be.

The first commit just re-arranges tests. It is probably best to only review the second commit.

Fixes #1089

@schmidt-sebastian schmidt-sebastian requested review from a team as code owners June 29, 2021 21:44
@product-auto-label product-auto-label bot added the api: firestore Issues related to the googleapis/nodejs-firestore API. label Jun 29, 2021
@google-cla google-cla bot added the cla: yes This human has signed the Contributor License Agreement. label Jun 29, 2021
@schmidt-sebastian schmidt-sebastian changed the title feat: add support for optimisitc transactions feat: add support for optimistic transactions Jun 29, 2021
Copy link

@thebrianchen thebrianchen left a comment

Choose a reason for hiding this comment

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

thanks for taking the time to explain this to me!

private _writeBatch: WriteBatch;
private _backoff: ExponentialBackoff;
private _requestTag: string;
private _locks = new Map<string, Precondition>();

Choose a reason for hiding this comment

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

optional: rename to readDocs or something similar to indicate that this map contains documents read in the transaction.

I was confused initially, since these aren't really "locks".

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Renamed to _readVersions and added comment. I also changed it to no longer store preconditions but just the underlying data as I think this makes more sense with the new name.

documentReader.transactionId = this._transactionId;
return documentReader.get(this._requestTag);

if (!this._optimisticLocking) {

Choose a reason for hiding this comment

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

optional: for future me's sake, please add a short comment (not necessarily here) to describe how optimistic locking is implemented with verify + not using transaction ids.

Otherwise, it seems like it's just an optimistic locking is just an option that you pass into the transaction request.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good suggestion. Comment added.

documentReader.transactionId = this._transactionId;
}

return documentReader.get(this._requestTag).then(docs => {

Choose a reason for hiding this comment

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

Do we have to handle the error case here? If we fail to fetch a document inside a tx, but the error is caught in the transaction callback, the final commit() won't have the preconditions set.

I think we need to chain the for loop after a silencePromise().

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't think we need to catch the error here. The error is bubbled up to the user. If the user choses to proceed, they know that we failed to fetch the document and cannot lock on it. We also do not know what to lock on as we have no information about the state of the document.

})
);
} else {
this._locks.set(

Choose a reason for hiding this comment

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

What's the expected behavior if an optimistic transaction reads the same document twice, but gets a different value each time?

My intuition says the tx should fail since the document value changed within the scope of the transaction, and if that's the intended behavior, we should check that the document isn't in the map.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, thanks for thinking about this. I copied the logic from Android.

@@ -609,6 +671,8 @@ function isRetryableTransactionError(error: GoogleError): boolean {
// IDs that have expired. While INVALID_ARGUMENT is generally not
// retryable, we retry this specific case.
return !!error.message.match(/transaction has expired/);
case StatusCode.FAILED_PRECONDITION:

Choose a reason for hiding this comment

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

question: why do we retry FAILED_PRECONDITION for optimistic locking?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The backend uses this error code when a document is updated after we read it during a transaction.

Choose a reason for hiding this comment

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

Please add a comment for this.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

There should already be a comment (in the last diff).

requestTag?: string;
retryCodes?: number[];
methodName?: FirestoreUnaryMethod;
preproccessor?: (request: api.ICommitRequest) => api.ICommitRequest;

Choose a reason for hiding this comment

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

Now that I've grasped the PR, the preprocessor implementation is so clean ✨

Copy link
Contributor Author

Choose a reason for hiding this comment

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

"Clean" is a much nicer word than "hacky" :)

);
});

it('does not combines precondition if already set', () => {

Choose a reason for hiding this comment

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

ultranit: s/combines/combine :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed

getDocument(/* transaction= */ ''),
commit(undefined, [set, verify])
);
});

Choose a reason for hiding this comment

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

optional (based on my above comments in transaction.ts): add test to check precondition if the same doc is read twice, add test to check that preconditions hold even if the document get() fails.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Test added

* are read during the transaction. These locks block other transactions,
* batched writes, and other non-transactional writes from changing that
* document. Any writes in a read-write transactions are committed once
* By default, read-write transactions obtain a pessimistic locks on all

Choose a reason for hiding this comment

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

I'll create a google doc to rewrite this section and add you.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks.

Copy link

@thebrianchen thebrianchen left a comment

Choose a reason for hiding this comment

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

lgtm, just add the updated comments.

@@ -609,6 +671,8 @@ function isRetryableTransactionError(error: GoogleError): boolean {
// IDs that have expired. While INVALID_ARGUMENT is generally not
// retryable, we retry this specific case.
return !!error.message.match(/transaction has expired/);
case StatusCode.FAILED_PRECONDITION:

Choose a reason for hiding this comment

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

Please add a comment for this.

@schmidt-sebastian
Copy link
Contributor Author

Changed this PR to specify a consistent read time once one has been obtained.

@schmidt-sebastian schmidt-sebastian added the kokoro:force-run Add this label to force Kokoro to re-run the tests. label Jul 2, 2021
@yoshi-kokoro yoshi-kokoro removed the kokoro:force-run Add this label to force Kokoro to re-run the tests. label Jul 2, 2021
@bcoe bcoe added the owlbot:run Add this label to trigger the Owlbot post processor. label Jul 2, 2021
@gcf-owl-bot gcf-owl-bot bot removed the owlbot:run Add this label to trigger the Owlbot post processor. label Jul 2, 2021
@bcoe bcoe added the kokoro:force-run Add this label to force Kokoro to re-run the tests. label Feb 4, 2022
@bcoe bcoe added the kokoro:run Add this label to force Kokoro to re-run the tests. label Feb 4, 2022
@PatrikValkovic
Copy link

PatrikValkovic commented Jan 15, 2024

What is the status of the PR? It is already approved but 2.5 years old without any updates. I would really appreciate this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
api: firestore Issues related to the googleapis/nodejs-firestore API. cla: yes This human has signed the Contributor License Agreement. kokoro:force-run Add this label to force Kokoro to re-run the tests. kokoro:run Add this label to force Kokoro to re-run the tests.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Support optimistic transactions as an option
5 participants