Skip to content

Commit 5d0b91c

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
1 parent 4571aaa commit 5d0b91c

3 files changed

Lines changed: 102 additions & 11 deletions

File tree

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: 7 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,12 @@ private function configureTransactionOptions(array $options = [])
152153
$transactionOptions['excludeTxnFromChangeStreams'] = $options['excludeTxnFromChangeStreams'];
153154
}
154155

156+
// The `readLockMode` can be set on the base options following convention or as a nested option if need be
157+
if (isset($options['readLockMode']) || isset($options['readWrite']['readLockMode'])) {
158+
$readLockModeOption = $options['readLockMode'] ?? $options['readWrite']['readLockMode'];
159+
$transactionOptions['readWrite']['readLockMode'] = $readLockModeOption;
160+
}
161+
155162
return $transactionOptions;
156163
}
157164

Spanner/tests/Unit/DatabaseTest.php

Lines changed: 89 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,94 @@ 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()->shouldBeCalledTimes(2)->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+
->shouldBeCalledTimes(2)
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 nested `readLockMode` set on `readWrite` transaction options
2179+
$database->runTransaction(
2180+
function (Transaction $t) use ($sql) {
2181+
// Run a fake query
2182+
$t->executeUpdate($sql);
2183+
2184+
// Simulate calling Transaction::commmit()
2185+
$prop = new \ReflectionProperty($t, 'state');
2186+
$prop->setAccessible(true);
2187+
$prop->setValue($t, Transaction::STATE_COMMITTED);
2188+
},
2189+
['transactionOptions' => ['readWrite' => ['readLockMode' => $expectedReadLockMode, ]]]
2190+
);
2191+
2192+
// Test TransactionOption array format with base level property set for readLockMode
2193+
// This helps test proper formating by the library to the format expected by Spanner backend (i.e. readLockMode should be inside readWrite)
2194+
$database->runTransaction(
2195+
function (Transaction $t) use ($sql) {
2196+
// Run a fake query
2197+
$t->executeUpdate($sql);
2198+
2199+
// Simulate calling Transaction::commmit()
2200+
$prop = new \ReflectionProperty($t, 'state');
2201+
$prop->setAccessible(true);
2202+
$prop->setValue($t, Transaction::STATE_COMMITTED);
2203+
},
2204+
['transactionOptions' => ['readLockMode' => $expectedReadLockMode,] ]
2205+
);
2206+
}
2207+
2208+
public function testTransactionWithReadLockMode()
2209+
{
2210+
$expectedReadLockMode = ReadLockMode::OPTIMISTIC;
2211+
2212+
$this->connection->beginTransaction(
2213+
Argument::that(function (array $args) use ($expectedReadLockMode) {
2214+
$this->assertArrayHasKey('transactionOptions', $args);
2215+
$this->assertArrayHasKey('readWrite', $args['transactionOptions']);
2216+
$this->assertArrayHasKey('readLockMode', $args['transactionOptions']['readWrite']);
2217+
$this->assertEquals(
2218+
$expectedReadLockMode,
2219+
$args['transactionOptions']['readWrite']['readLockMode'],
2220+
"The read lock mode received was {$args['transactionOptions']['readWrite']['readLockMode']} does not match expected {$expectedReadLockMode}"
2221+
);
2222+
return true;
2223+
}))
2224+
->shouldBeCalled()
2225+
->willReturn(['id' => self::TRANSACTION]);
2226+
2227+
$t = $this->database->transaction(['transactionOptions' => ['readWrite' => ['readLockMode' => $expectedReadLockMode, ]]]);
2228+
$this->assertInstanceOf(Transaction::class, $t);
2229+
}
2230+
21422231
private function createStreamingAPIArgs()
21432232
{
21442233
$row = ['id' => 1];

0 commit comments

Comments
 (0)