Skip to content

Commit 4d3cea5

Browse files
committed
feat: Support for Spanner ReadLockMode
tests: Add in unit tests for ReadLockMode option chore: Fix lint issues chore: Fix lint issues chore: Fix lint issues feat: Add ability to set this ReadLockMode through operations chore: Cleanup code and remove debug logging chore: Lint fixes chore: Remove one test as per review comments
1 parent 4571aaa commit 4d3cea5

5 files changed

Lines changed: 182 additions & 12 deletions

File tree

Spanner/src/Connection/Grpc.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1107,6 +1107,12 @@ public function beginTransaction(array $args)
11071107
$readWrite = new ReadWrite();
11081108
$options->setReadWrite($readWrite);
11091109
$args = $this->addLarHeader($args, $this->larEnabled);
1110+
1111+
if(isset($transactionOptions['readWrite']['readLockMode'])) {
1112+
// Nested option `readLockMode` inside `readWrite` transactions
1113+
$readLockModeOption = $transactionOptions['readWrite']['readLockMode'];
1114+
$options->getReadWrite()->setReadLockMode($readLockModeOption);
1115+
}
11101116
} elseif (isset($transactionOptions['partitionedDml'])) {
11111117
$pdml = new PartitionedDml();
11121118
$options->setPartitionedDml($pdml);
@@ -1576,6 +1582,19 @@ private function formatTransactionOptions(array $transactionOptions)
15761582
$transactionOptions['readOnly'] = $ro;
15771583
}
15781584

1585+
if (isset($transactionOptions['readWrite'])) {
1586+
$rw = $transactionOptions['readWrite'];
1587+
1588+
// Format nested options inside readWrite transaction
1589+
if (isset($transactionOptions['readLockMode'])) {
1590+
$rw['readLockMode'] = $transactionOptions['readLockMode'];
1591+
1592+
// Unset the readLockMode key on the base options array. If we don't do this it causes issues in the serializer for TransactionOptions
1593+
unset($transactionOptions['readLockMode']);
1594+
}
1595+
1596+
$transactionOptions['readWrite'] = $rw;
1597+
}
15791598
return $transactionOptions;
15801599
}
15811600

Spanner/src/Database.php

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -775,10 +775,8 @@ public function snapshot(array $options = [])
775775
* If you wish Google Cloud PHP to handle retry logic for you (recommended
776776
* for most cases), use {@see \Google\Cloud\Spanner\Database::runTransaction()}.
777777
*
778-
* Please note that once a transaction reads data, it will lock the read
779-
* data, preventing other users from modifying that data. For this reason,
780-
* it is important that every transaction commits or rolls back as early as
781-
* possible. Do not hold transactions open longer than necessary.
778+
* Please note for locking semantics and defaults for the transactions
779+
* use {@see \Google\Cloud\Spanner\V1\TransactionOptions\ReadWrite\ReadLockMode}
782780
*
783781
* Example:
784782
* ```
@@ -812,8 +810,8 @@ public function transaction(array $options = [])
812810
throw new \BadMethodCallException('Nested transactions are not supported by this client.');
813811
}
814812

815-
// There isn't anything configurable here.
816-
$options['transactionOptions'] = $this->configureTransactionOptions();
813+
// Configure readWrite options here. Any nested options for readWrite should be added to this call
814+
$options['transactionOptions'] = $this->configureTransactionOptions($options['transactionOptions'] ?? []);
817815

818816
$session = $this->selectSession(
819817
SessionPoolInterface::CONTEXT_READWRITE,
@@ -840,10 +838,8 @@ public function transaction(array $options = [])
840838
* exception types will immediately bubble up and will interrupt the retry
841839
* operation.
842840
*
843-
* Please note that once a transaction reads data, it will lock the read
844-
* data, preventing other users from modifying that data. For this reason,
845-
* it is important that every transaction commits or rolls back as early as
846-
* possible. Do not hold transactions open longer than necessary.
841+
* Please note for locking semantics and defaults for the transactions
842+
* use {@see \Google\Cloud\Spanner\V1\TransactionOptions\ReadWrite\ReadLockMode}
847843
*
848844
* Please also note that nested transactions are NOT supported by this client.
849845
* Attempting to call `runTransaction` inside a transaction callable will
@@ -920,7 +916,6 @@ public function runTransaction(callable $operation, array $options = [])
920916
'maxRetries' => self::MAX_RETRIES,
921917
];
922918

923-
// There isn't anything configurable here.
924919
$options['transactionOptions'] = $this->configureTransactionOptions($options['transactionOptions'] ?? []);
925920

926921
$session = $this->selectSession(

Spanner/src/TransactionConfigurationTrait.php

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
use Google\Cloud\Core\ArrayTrait;
2121
use Google\Cloud\Spanner\Session\SessionPoolInterface;
22+
use Google\Cloud\Spanner\V1\TransactionOptions\ReadWrite\ReadLockMode as ReadLockMode;
2223

2324
/**
2425
* Configure transaction selection for read, executeSql, rollback and commit.
@@ -143,7 +144,7 @@ private function transactionOptions(array &$options, array $previous = [])
143144
}
144145

145146
private function configureTransactionOptions(array $options = [])
146-
{
147+
{ // Purva: this method is being called twice somehow once with ['readLockMode'] and then with the correct formatted ['readWrite']['readLockMode']. Second call is removing the option due to condition on L158
147148
$transactionOptions = [
148149
'readWrite' => []
149150
];
@@ -152,6 +153,15 @@ private function configureTransactionOptions(array $options = [])
152153
$transactionOptions['excludeTxnFromChangeStreams'] = $options['excludeTxnFromChangeStreams'];
153154
}
154155

156+
// Allow for proper configuring of the `readLockMode` if it's set as a base or nested option
157+
if (isset($options['readLockMode'])) {
158+
$transactionOptions['readWrite']['readLockMode'] = $options['readLockMode'];
159+
}
160+
161+
if (isset($options['readWrite']['readLockMode'])) {
162+
$transactionOptions['readWrite']['readLockMode'] = $options['readWrite']['readLockMode'];
163+
}
164+
155165
return $transactionOptions;
156166
}
157167

Spanner/tests/Unit/DatabaseTest.php

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
use Google\Cloud\Spanner\V1\SpannerClient;
5353
use Google\Cloud\Spanner\V1\Transaction as TransactionProto;
5454
use Google\Cloud\Spanner\V1\TransactionOptions;
55+
use Google\Cloud\Spanner\V1\TransactionOptions\ReadWrite\ReadLockMode as ReadLockMode;
5556
use Google\Rpc\Code;
5657
use PHPUnit\Framework\TestCase;
5758
use Prophecy\Argument;
@@ -2139,6 +2140,80 @@ public function testBatchWriteWithExcludeTxnFromChangeStreams()
21392140
]);
21402141
}
21412142

2143+
public function testRunTransactionWithReadLockMode()
2144+
{
2145+
$expectedReadLockMode = ReadLockMode::OPTIMISTIC;
2146+
2147+
$gapic = $this->prophesize(SpannerClient::class);
2148+
2149+
$sessName = SpannerClient::sessionName(self::PROJECT, self::INSTANCE, self::DATABASE, self::SESSION);
2150+
$session = new SessionProto(['name' => $sessName]);
2151+
$resultSet = new ResultSet(['stats' => new ResultSetStats(['row_count_exact' => 0])]);
2152+
$gapic->createSession(Argument::cetera())->shouldBeCalled()->willReturn($session);
2153+
$gapic->deleteSession(Argument::cetera())->shouldBeCalled();
2154+
2155+
$sql = 'SELECT example FROM sql_query';
2156+
$stream = $this->prophesize(ServerStream::class);
2157+
$stream->readAll()->shouldBeCalledOnce()->willReturn([$resultSet]);
2158+
$gapic->executeStreamingSql($sessName, $sql, Argument::that(function (array $options) use ($expectedReadLockMode) {
2159+
$this->assertArrayHasKey('transaction', $options);
2160+
$this->assertNotNull($transactionOptions = $options['transaction']->getBegin());
2161+
$this->assertNotNull($readWriteTxnOptions = $transactionOptions->getReadWrite());
2162+
$this->assertNotNull($readLockModeOption = $readWriteTxnOptions->getReadLockMode());
2163+
$this->assertEquals($expectedReadLockMode, $readLockModeOption, "The read lock mode received was {$readLockModeOption} does not match expected {$expectedReadLockMode}");
2164+
return true;
2165+
}))
2166+
->shouldBeCalledOnce()
2167+
->willReturn($stream->reveal());
2168+
2169+
$database = new Database(
2170+
new Grpc(['gapicSpannerClient' => $gapic->reveal()]),
2171+
$this->instance,
2172+
$this->lro->reveal(),
2173+
$this->lroCallables,
2174+
self::PROJECT,
2175+
self::DATABASE
2176+
);
2177+
2178+
// Test TransactionOption array format with base level property set for readLockMode
2179+
// This helps test proper formating by the library to the format expected by Spanner backend (i.e. readLockMode should be inside readWrite)
2180+
$database->runTransaction(
2181+
function (Transaction $t) use ($sql) {
2182+
// Run a fake query
2183+
$t->executeUpdate($sql);
2184+
2185+
// Simulate calling Transaction::commmit()
2186+
$prop = new \ReflectionProperty($t, 'state');
2187+
$prop->setAccessible(true);
2188+
$prop->setValue($t, Transaction::STATE_COMMITTED);
2189+
},
2190+
['transactionOptions' => ['readLockMode' => $expectedReadLockMode,] ]
2191+
);
2192+
}
2193+
2194+
public function testTransactionWithReadLockMode()
2195+
{
2196+
$expectedReadLockMode = ReadLockMode::OPTIMISTIC;
2197+
2198+
$this->connection->beginTransaction(
2199+
Argument::that(function (array $args) use ($expectedReadLockMode) {
2200+
$this->assertArrayHasKey('transactionOptions', $args);
2201+
$this->assertArrayHasKey('readWrite', $args['transactionOptions']);
2202+
$this->assertArrayHasKey('readLockMode', $args['transactionOptions']['readWrite']);
2203+
$this->assertEquals(
2204+
$expectedReadLockMode,
2205+
$args['transactionOptions']['readWrite']['readLockMode'],
2206+
"The read lock mode received was {$args['transactionOptions']['readWrite']['readLockMode']} does not match expected {$expectedReadLockMode}"
2207+
);
2208+
return true;
2209+
}))
2210+
->shouldBeCalled()
2211+
->willReturn(['id' => self::TRANSACTION]);
2212+
2213+
$t = $this->database->transaction(['transactionOptions' => ['readLockMode' => $expectedReadLockMode, ]]);
2214+
$this->assertInstanceOf(Transaction::class, $t);
2215+
}
2216+
21422217
private function createStreamingAPIArgs()
21432218
{
21442219
$row = ['id' => 1];

Spanner/tests/Unit/OperationTest.php

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
use PHPUnit\Framework\TestCase;
4646
use Prophecy\Argument;
4747
use Prophecy\PhpUnit\ProphecyTrait;
48+
use Google\Cloud\Spanner\V1\TransactionOptions\ReadWrite\ReadLockMode as ReadLockMode;
4849

4950
/**
5051
* @group spanner
@@ -421,6 +422,76 @@ public function testExecuteAndExecuteUpdateWithExcludeTxnFromChangeStreams()
421422
]);
422423
}
423424

425+
public function testTransactionWithReadLockMode()
426+
{
427+
428+
$expectedReadLockMode = ReadLockMode::OPTIMISTIC;
429+
$gapic = $this->prophesize(SpannerClient::class);
430+
$gapic->beginTransaction(
431+
self::SESSION,
432+
Argument::that(function (TransactionOptions $options) use ($expectedReadLockMode) {
433+
$this->assertNotNull($readWriteTxnOptions = $options->getReadWrite());
434+
$this->assertNotNull($readLockModeOption = $readWriteTxnOptions->getReadLockMode());
435+
$this->assertEquals($expectedReadLockMode, $readLockModeOption,
436+
"The read lock mode received was {$readLockModeOption} does not match expected {$expectedReadLockMode}");
437+
return true;
438+
}),
439+
Argument::type('array')
440+
)
441+
->shouldBeCalled()
442+
->willReturn(new TransactionProto(['id' => 'foo']));
443+
444+
$operation = new Operation(
445+
new Grpc(['gapicSpannerClient' => $gapic->reveal()]),
446+
true
447+
);
448+
449+
$transaction = $operation->transaction($this->session, [
450+
'transactionOptions' => ['readWrite' => [], 'readLockMode' => $expectedReadLockMode, ]
451+
]);
452+
453+
$this->assertEquals('foo', $transaction->id());
454+
}
455+
456+
public function testExecuteAndExecuteUpdateWithReadLockMode()
457+
{
458+
$expectedReadLockMode = ReadLockMode::OPTIMISTIC;
459+
$sql = 'SELECT example FROM sql_query';
460+
461+
$resultSet = new ResultSet(['stats' => new ResultSetStats(['row_count_exact' => 0])]);
462+
$stream = $this->prophesize(ServerStream::class);
463+
$stream->readAll()->shouldBeCalledTimes(2)->willReturn([$resultSet]);
464+
465+
$gapic = $this->prophesize(SpannerClient::class);
466+
$gapic->executeStreamingSql(self::SESSION, $sql, Argument::that(function (array $options) use ($expectedReadLockMode)
467+
{
468+
$this->assertArrayHasKey('transaction', $options);
469+
$this->assertNotNull($transactionOptions = $options['transaction']->getBegin());
470+
$this->assertNotNull($readWriteTxnOptions = $transactionOptions->getReadWrite());
471+
$this->assertNotNull($readLockModeOption = $readWriteTxnOptions->getReadLockMode());
472+
$this->assertEquals($expectedReadLockMode, $readLockModeOption,
473+
"The read lock mode received was {$readLockModeOption} does not match expected {$expectedReadLockMode}");
474+
return true;
475+
}))
476+
->shouldBeCalledTimes(2)
477+
->willReturn($stream->reveal());
478+
479+
$operation = new Operation(
480+
new Grpc(['gapicSpannerClient' => $gapic->reveal()]),
481+
true
482+
);
483+
484+
$operation->execute($this->session, $sql, [
485+
'transaction' => ['begin' => ['readWrite' => [], 'readLockMode' => $expectedReadLockMode,]]
486+
]);
487+
488+
$transaction = $this->prophesize(Transaction::class)->reveal();
489+
490+
$operation->executeUpdate($this->session, $transaction, $sql, [
491+
'transaction' => ['begin' => ['readWrite' => [],'readLockMode' => $expectedReadLockMode,]]
492+
]);
493+
}
494+
424495
public function testSnapshot()
425496
{
426497
$this->connection->beginTransaction(Argument::allOf(

0 commit comments

Comments
 (0)