Skip to content

Commit 1aae26c

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: Fix style warnings
1 parent 4571aaa commit 1aae26c

5 files changed

Lines changed: 197 additions & 11 deletions

File tree

Spanner/src/Connection/Grpc.php

Lines changed: 20 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,20 @@ 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.
1593+
// If we don't do this it causes issues in the serializer for TransactionOptions
1594+
unset($transactionOptions['readLockMode']);
1595+
}
1596+
1597+
$transactionOptions['readWrite'] = $rw;
1598+
}
15791599
return $transactionOptions;
15801600
}
15811601

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: 10 additions & 0 deletions
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.
@@ -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: 83 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,88 @@ 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)
2159+
use ($expectedReadLockMode) {
2160+
$this->assertArrayHasKey('transaction', $options);
2161+
$this->assertNotNull($transactionOptions = $options['transaction']->getBegin());
2162+
$this->assertNotNull($readWriteTxnOptions = $transactionOptions->getReadWrite());
2163+
$this->assertNotNull($readLockModeOption = $readWriteTxnOptions->getReadLockMode());
2164+
$this->assertEquals(
2165+
$expectedReadLockMode,
2166+
$readLockModeOption,
2167+
"The read lock mode received was {$readLockModeOption} does not match expected {$expectedReadLockMode}"
2168+
);
2169+
return true;
2170+
}))
2171+
->shouldBeCalledOnce()
2172+
->willReturn($stream->reveal());
2173+
2174+
$database = new Database(
2175+
new Grpc(['gapicSpannerClient' => $gapic->reveal()]),
2176+
$this->instance,
2177+
$this->lro->reveal(),
2178+
$this->lroCallables,
2179+
self::PROJECT,
2180+
self::DATABASE
2181+
);
2182+
2183+
// Test TransactionOption array format with base level property set for readLockMode
2184+
// This helps test proper formating by the library to the format expected by Spanner backend
2185+
// (i.e. readLockMode should be inside readWrite)
2186+
$database->runTransaction(
2187+
function (Transaction $t) use ($sql) {
2188+
// Run a fake query
2189+
$t->executeUpdate($sql);
2190+
2191+
// Simulate calling Transaction::commmit()
2192+
$prop = new \ReflectionProperty($t, 'state');
2193+
$prop->setAccessible(true);
2194+
$prop->setValue($t, Transaction::STATE_COMMITTED);
2195+
},
2196+
['transactionOptions' => ['readLockMode' => $expectedReadLockMode,] ]
2197+
);
2198+
}
2199+
2200+
public function testTransactionWithReadLockMode()
2201+
{
2202+
$expectedReadLockMode = ReadLockMode::OPTIMISTIC;
2203+
2204+
$this->connection->beginTransaction(
2205+
Argument::that(function (array $args) use ($expectedReadLockMode) {
2206+
$this->assertArrayHasKey('transactionOptions', $args);
2207+
$this->assertArrayHasKey('readWrite', $args['transactionOptions']);
2208+
$this->assertArrayHasKey('readLockMode', $args['transactionOptions']['readWrite']);
2209+
$this->assertEquals(
2210+
$expectedReadLockMode,
2211+
$args['transactionOptions']['readWrite']['readLockMode'],
2212+
"The read lock mode received was {$args['transactionOptions']['readWrite']['readLockMode']} ".
2213+
"does not match expected {$expectedReadLockMode}"
2214+
);
2215+
return true;
2216+
})
2217+
)
2218+
->shouldBeCalled()
2219+
->willReturn(['id' => self::TRANSACTION]);
2220+
2221+
$t = $this->database->transaction(['transactionOptions' => ['readLockMode' => $expectedReadLockMode, ]]);
2222+
$this->assertInstanceOf(Transaction::class, $t);
2223+
}
2224+
21422225
private function createStreamingAPIArgs()
21432226
{
21442227
$row = ['id' => 1];

Spanner/tests/Unit/OperationTest.php

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

0 commit comments

Comments
 (0)