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: inline BeginTransaction with first statement #1692

Merged
merged 28 commits into from
Nov 7, 2022

Conversation

ko3a4ok
Copy link
Contributor

@ko3a4ok ko3a4ok commented Aug 6, 2022

Add inline BeginTransaction for the first read or executeSql request in a transaction.

Each transaction doesn't call beginTransanction before its start. Instead, all creation transaction parameters are passed within the first read or executeSql request.
In case of a transaction restart, beginTransaction is called explicitly.

@ko3a4ok ko3a4ok requested review from a team as code owners August 6, 2022 02:31
@product-auto-label product-auto-label bot added size: m Pull request size is medium. api: spanner Issues related to the googleapis/nodejs-spanner API. labels Aug 6, 2022
@@ -573,6 +563,8 @@ export class Snapshot extends EventEmitter {

if (this.id) {
transaction.id = this.id as Uint8Array;
} else if (typeof this._options.readWrite !== 'undefined') {
transaction.begin = this._options;
Copy link
Contributor

Choose a reason for hiding this comment

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

Would you mind adding a test that verifies the following:

  1. A query is the first statement in a read/write transaction. A transaction ID is successfully returned by initial request.
  2. One or more PartialResultSets are returned by the stream, with (at least) one of them returning a resume token.
  3. The stream fails halfway with an UNAVAILABLE error and the stream is restarted with a resume token.

Step 3 should use the transaction ID that was returned by step 1, and not include a BeginTransaction option.

(https://github.com/googleapis/nodejs-spanner/pull/1253/files is a very old implementation for the same. Some of the tests there might be re-usable.)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added. thanks a lot for this comment: it helped to find a bug in the code

@@ -1034,6 +1029,8 @@ export class Snapshot extends EventEmitter {
const transaction: spannerClient.spanner.v1.ITransactionSelector = {};
if (this.id) {
Copy link
Contributor

Choose a reason for hiding this comment

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

This does not take into account that a transaction could in theory have multiple requests in flight at the same time. If a transaction starts out with sending two SELECT statements to the backend, it might very well be that the first that is sent has not yet returned a transaction id before the second is being sent. That will cause both requests to include a BeginTransaction option.

Consider the following test case (the getRowCountFromStreamingSql function is defined in test/Spanner.ts):

    it('should only inline one begin transaction', async () => {
      const database = newTestDatabase();
      await database.runTransactionAsync(async tx => {
        const rowCount1 = getRowCountFromStreamingSql(tx, {sql: selectSql});
        const rowCount2 = getRowCountFromStreamingSql(tx, {sql: selectSql});
        await Promise.all([rowCount1, rowCount2]);
        await tx.commit();
      });
      await database.close();

      let request = spannerMock.getRequests().find(val => {
        return (val as v1.ExecuteSqlRequest).sql;
      }) as v1.ExecuteSqlRequest;
      assert.ok(request, 'no ExecuteSqlRequest found');
      assert.deepStrictEqual(request.transaction!.begin!.readWrite, {});
      assert.strictEqual(request.sql, selectSql);

      request = spannerMock
        .getRequests()
        .slice()
        .reverse()
        .find(val => {
          return (val as v1.ExecuteSqlRequest).sql;
        }) as v1.ExecuteSqlRequest;
      assert.ok(request, 'no ExecuteSqlRequest found');
      assert.strictEqual(request.sql, selectSql);
      assert.ok(request.transaction!.id, 'TransactionID is not set.');
      const beginTxnRequest = spannerMock.getRequests().find(val => {
        return (val as v1.BeginTransactionRequest).options?.readWrite;
      }) as v1.BeginTransactionRequest;
      assert.ok(!beginTxnRequest, 'beginTransaction was called');
    });

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You're absolutely right. Added a lock that prevents a multiple inline begin transaction at the same time. Thanks!

@asthamohta asthamohta added the kokoro:force-run Add this label to force Kokoro to re-run the tests. label Aug 11, 2022
@yoshi-kokoro yoshi-kokoro removed the kokoro:force-run Add this label to force Kokoro to re-run the tests. label Aug 11, 2022
…on happens; make sure stream requests uses a transaction id from the first response even if the stream fails halfway with an UNAVAILABLE error
@product-auto-label product-auto-label bot added size: l Pull request size is large. and removed size: m Pull request size is medium. labels Aug 12, 2022
json,
jsonOptions,
maxResumeRetries,
}).on('response', response => {
Copy link
Contributor

@surbhigarg92 surbhigarg92 Aug 22, 2022

Choose a reason for hiding this comment

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

What would happen if the call fails?
As per the design we should make an explicit begin transaction call if the call fails.

cc: @olavloite

Copy link
Contributor Author

Choose a reason for hiding this comment

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

if the call fails, the transaction is created explicitly with "BeginTransaction" rpc call.
It's covered in the test: "should use beginTransaction on retry".

Copy link
Contributor

Choose a reason for hiding this comment

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

Is this done at the backend or do we explicitly call begin transaction from the client side?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

from the client side

@@ -573,6 +570,8 @@ export class Snapshot extends EventEmitter {

if (this.id) {
transaction.id = this.id as Uint8Array;
} else if (typeof this._options.readWrite !== 'undefined') {
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this rather be else if(this._options.readWrite)

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, fixed, thanks

@surbhigarg92 surbhigarg92 added kokoro:force-run Add this label to force Kokoro to re-run the tests. owlbot:run Add this label to trigger the Owlbot post processor. labels Sep 14, 2022
@gcf-owl-bot gcf-owl-bot bot removed the owlbot:run Add this label to trigger the Owlbot post processor. label Sep 14, 2022
@yoshi-kokoro yoshi-kokoro removed the kokoro:force-run Add this label to force Kokoro to re-run the tests. label Sep 14, 2022
@surbhigarg92
Copy link
Contributor

@ko3a4ok Can you please fix the failing test cases ?

@surbhigarg92
Copy link
Contributor

surbhigarg92 commented Sep 14, 2022

@ko3a4ok We should also remove the transaction.begin call at line and add the appropriate test cases for the same.

@ko3a4ok
Copy link
Contributor Author

ko3a4ok commented Sep 14, 2022

@ko3a4ok We should also remove the transaction.begin call at line and add the appropriate test cases for the same.

@surbhigarg92, unfortunately, if we just remove it, the explicit beginTransaction call won't be called on the retry. I updated to call it only after the first attempt. Please let me know if it makes sense, or is it better to move the call transaction.begin() somewhere here.

@surbhigarg92
Copy link
Contributor

@ko3a4ok We should also remove the transaction.begin call at line and add the appropriate test cases for the same.

@surbhigarg92, unfortunately, if we just remove it, the explicit beginTransaction call won't be called on the retry. I updated to call it only after the first attempt. Please let me know if it makes sense, or is it better to move the call transaction.begin() somewhere here.

This looks fine. Only thing I am wondering is that with this implementation, we are calling transaction.begin() in all possible failure cases.
@olavloite Do you think there will be any particular error case where we wan't to skip calling transaction.begin()

@ko3a4ok
Copy link
Contributor Author

ko3a4ok commented Sep 20, 2022

@ko3a4ok I believe we are missing 2 use cases for Inline BeginTransaction implementation.

  1. Mutation - Consider a transaction which contains only mutation operations. In that case, first rpc in the transaction will be commit transaction. To handle this scenario , we would need to make an explicit begin transaction rpc.
  2. ExecuteBatchDML - Transaction needs to be inlined for ExecuteBatchDML operation as well. Special handling is required for this use case.

ExecuteBatchDml is also slightly odd since either the call itself can fail, or it can return success with a non-OK Status in the ExecuteBatchDmlResponse. In the latter case, there may still be one or more ResultSets returned, with the first containing a transaction ID in its ResultSetMetadata. The upshot is that the client should check ExecuteBatchDmlResponse::result_set for a transaction prior to considering ExecuteBatchDmlResponse::status.

Regarding 1.: in this case, we should not create any transaction at all because CommitRequest will include singleUseTransaction parameter, and transactionId will be empty. This is how it works now, and inlined begin transaction shouldn't affect this implementation.

Regarding 2.: I completely forgot about that, thanks for bringing this up. Added in 2caaa0f

@surbhigarg92 surbhigarg92 added the automerge Merge the pull request once unit tests and other checks pass. label Nov 2, 2022
@surbhigarg92 surbhigarg92 added the kokoro:force-run Add this label to force Kokoro to re-run the tests. label Nov 2, 2022
@yoshi-kokoro yoshi-kokoro removed the kokoro:force-run Add this label to force Kokoro to re-run the tests. label Nov 2, 2022
@gcf-merge-on-green
Copy link
Contributor

Merge-on-green attempted to merge your PR for 6 hours, but it was not mergeable because either one of your required status checks failed, one of your required reviews was not approved, or there is a do not merge label. Learn more about your required status checks here: https://help.github.com/en/github/administering-a-repository/enabling-required-status-checks. You can remove and reapply the label to re-run the bot.

@gcf-merge-on-green gcf-merge-on-green bot removed the automerge Merge the pull request once unit tests and other checks pass. label Nov 2, 2022
@surbhigarg92 surbhigarg92 added the kokoro:force-run Add this label to force Kokoro to re-run the tests. label Nov 3, 2022
@yoshi-kokoro yoshi-kokoro removed the kokoro:force-run Add this label to force Kokoro to re-run the tests. label Nov 3, 2022
@surbhigarg92 surbhigarg92 added the kokoro:force-run Add this label to force Kokoro to re-run the tests. label Nov 4, 2022
@yoshi-kokoro yoshi-kokoro removed the kokoro:force-run Add this label to force Kokoro to re-run the tests. label Nov 4, 2022
@surbhigarg92 surbhigarg92 added the kokoro:force-run Add this label to force Kokoro to re-run the tests. label Nov 4, 2022
@yoshi-kokoro yoshi-kokoro removed the kokoro:force-run Add this label to force Kokoro to re-run the tests. label Nov 4, 2022
@surbhigarg92 surbhigarg92 added the kokoro:force-run Add this label to force Kokoro to re-run the tests. label Nov 7, 2022
@yoshi-kokoro yoshi-kokoro removed kokoro:force-run Add this label to force Kokoro to re-run the tests. labels Nov 7, 2022
@surbhigarg92 surbhigarg92 added the kokoro:force-run Add this label to force Kokoro to re-run the tests. label Nov 7, 2022
@yoshi-kokoro yoshi-kokoro removed the kokoro:force-run Add this label to force Kokoro to re-run the tests. label Nov 7, 2022
@surbhigarg92 surbhigarg92 added the owlbot:run Add this label to trigger the Owlbot post processor. label Nov 7, 2022
@gcf-owl-bot gcf-owl-bot bot removed the owlbot:run Add this label to trigger the Owlbot post processor. label Nov 7, 2022
@surbhigarg92 surbhigarg92 merged commit d1b95d2 into googleapis:main Nov 7, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
api: spanner Issues related to the googleapis/nodejs-spanner API. size: l Pull request size is large.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants