-
Notifications
You must be signed in to change notification settings - Fork 148
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
base: main
Are you sure you want to change the base?
Conversation
c8785c2
to
cb1a009
Compare
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 taking the time to explain this to me!
dev/src/transaction.ts
Outdated
private _writeBatch: WriteBatch; | ||
private _backoff: ExponentialBackoff; | ||
private _requestTag: string; | ||
private _locks = new Map<string, Precondition>(); |
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.
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".
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.
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) { |
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.
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.
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.
Good suggestion. Comment added.
documentReader.transactionId = this._transactionId; | ||
} | ||
|
||
return documentReader.get(this._requestTag).then(docs => { |
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.
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()
.
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.
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.
dev/src/transaction.ts
Outdated
}) | ||
); | ||
} else { | ||
this._locks.set( |
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.
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.
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.
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: |
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.
question: why do we retry FAILED_PRECONDITION for optimistic locking?
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.
The backend uses this error code when a document is updated after we read it during a transaction.
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.
Please add a comment for this.
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.
There should already be a comment (in the last diff).
requestTag?: string; | ||
retryCodes?: number[]; | ||
methodName?: FirestoreUnaryMethod; | ||
preproccessor?: (request: api.ICommitRequest) => api.ICommitRequest; |
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.
Now that I've grasped the PR, the preprocessor implementation is so clean ✨
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.
"Clean" is a much nicer word than "hacky" :)
dev/test/transaction.ts
Outdated
); | ||
}); | ||
|
||
it('does not combines precondition if already set', () => { |
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.
ultranit: s/combines/combine :)
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.
Fixed
getDocument(/* transaction= */ ''), | ||
commit(undefined, [set, verify]) | ||
); | ||
}); |
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.
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.
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.
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 |
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.
I'll create a google doc to rewrite this section and add you.
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.
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.
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: |
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.
Please add a comment for this.
Changed this PR to specify a consistent read time once one has been obtained. |
What is the status of the PR? It is already approved but 2.5 years old without any updates. I would really appreciate this. |
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