Skip to content

Commit e21a5b5

Browse files
authored
fix(spanner): client-level read lock mode setting (#9084)
Allows the read lock mode to be set at the client-level. The existing support to set the read lock mode at a transaction level can be used to override the client-level setting at a per-transaction level. Also adds unit tests for setting the isolation level at the client level and being able to override it at the transaction level.
1 parent cb2837c commit e21a5b5

5 files changed

Lines changed: 273 additions & 2 deletions

File tree

Spanner/src/Database.php

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
use Google\Cloud\Spanner\V1\Mutation\Delete;
5656
use Google\Cloud\Spanner\V1\Mutation\Write;
5757
use Google\Cloud\Spanner\V1\TransactionOptions\IsolationLevel;
58+
use Google\Cloud\Spanner\V1\TransactionOptions\ReadWrite\ReadLockMode;
5859
use Google\Cloud\Spanner\V1\TypeCode;
5960
use Google\LongRunning\ListOperationsRequest;
6061
use Google\LongRunning\Operation as OperationProto;
@@ -128,6 +129,7 @@ class Database
128129
private CacheItemPoolInterface $cacheItemPool;
129130
private array $info;
130131
private int $isolationLevel;
132+
private int $readLockMode;
131133
private TransactionOptionsBuilder $transactionOptionsBuilder;
132134

133135
/**
@@ -175,6 +177,7 @@ public function __construct(
175177
$this->returnInt64AsObject = $options['returnInt64AsObject'] ?? false;
176178
$this->info = $options['database'] ?? [];
177179
$this->isolationLevel = $options['isolationLevel'] ?? IsolationLevel::ISOLATION_LEVEL_UNSPECIFIED;
180+
$this->readLockMode = $options['readLockMode'] ?? ReadLockMode::READ_LOCK_MODE_UNSPECIFIED;
178181
$this->operation = new Operation(
179182
$this->spannerClient,
180183
$serializer,
@@ -807,7 +810,8 @@ public function transaction(array $options = []): Transaction
807810
$txnOptions = $options['transactionOptions'] ?? [];
808811
$options['transactionOptions'] = $this->transactionOptionsBuilder
809812
->configureReadWriteTransactionOptions($txnOptions + [
810-
'isolationLevel' => $this->isolationLevel
813+
'isolationLevel' => $this->isolationLevel,
814+
'readLockMode' => $this->readLockMode
811815
]);
812816

813817
return $this->operation->transaction($this->session, $options);
@@ -915,7 +919,8 @@ public function runTransaction(callable $operation, array $options = []): mixed
915919
$txnOptions = $options['transactionOptions'] ?? [];
916920
$options['transactionOptions'] = $this->transactionOptionsBuilder
917921
->configureReadWriteTransactionOptions($txnOptions + [
918-
'isolationLevel' => $this->isolationLevel
922+
'isolationLevel' => $this->isolationLevel,
923+
'readLockMode' => $this->readLockMode
919924
]);
920925

921926
$attempt = 0;

Spanner/src/Instance.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
use Google\Cloud\Spanner\Session\SessionCache;
4545
use Google\Cloud\Spanner\V1\Client\SpannerClient as GapicSpannerClient;
4646
use Google\Cloud\Spanner\V1\TransactionOptions\IsolationLevel;
47+
use Google\Cloud\Spanner\V1\TransactionOptions\ReadWrite\ReadLockMode;
4748
use Google\LongRunning\ListOperationsRequest;
4849
use Google\LongRunning\Operation as OperationProto;
4950
use InvalidArgumentException;
@@ -79,6 +80,7 @@ class Instance
7980
private bool $returnInt64AsObject;
8081
private array $info;
8182
private int $isolationLevel;
83+
private int $readLockMode;
8284
private CacheItemPoolInterface|null $cacheItemPool;
8385

8486
/**
@@ -122,6 +124,7 @@ public function __construct(
122124
$this->name = $this->fullyQualifiedInstanceName($name);
123125
$this->directedReadOptions = $options['directedReadOptions'] ?? [];
124126
$this->isolationLevel = $options['isolationLevel'] ?? IsolationLevel::ISOLATION_LEVEL_UNSPECIFIED;
127+
$this->readLockMode = $options['readLockMode'] ?? ReadLockMode::READ_LOCK_MODE_UNSPECIFIED;
125128
$this->routeToLeader = $options['routeToLeader'] ?? true;
126129
$this->defaultQueryOptions = $options['defaultQueryOptions'] ?? [];
127130
$this->returnInt64AsObject = $options['returnInt64AsObject'] ?? false;
@@ -571,6 +574,7 @@ public function database(string $name, array $options = []): Database
571574
'defaultQueryOptions' => $this->defaultQueryOptions,
572575
'returnInt64AsObject' => $this->returnInt64AsObject,
573576
'isolationLevel' => $this->isolationLevel,
577+
'readLockMode' => $this->readLockMode,
574578
]
575579
);
576580
}

Spanner/src/SpannerClient.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
use Google\Cloud\Spanner\Middleware\SpannerMiddleware;
4343
use Google\Cloud\Spanner\V1\Client\SpannerClient as GapicSpannerClient;
4444
use Google\Cloud\Spanner\V1\TransactionOptions\IsolationLevel;
45+
use Google\Cloud\Spanner\V1\TransactionOptions\ReadWrite\ReadLockMode;
4546
use Google\LongRunning\Operation as OperationProto;
4647
use Google\Protobuf\Duration;
4748
use Psr\Cache\CacheItemPoolInterface;
@@ -130,6 +131,7 @@ class SpannerClient
130131
private bool $routeToLeader;
131132
private array $defaultQueryOptions;
132133
private int $isolationLevel;
134+
private int $readLockMode;
133135
private CacheItemPoolInterface|null $cacheItemPool;
134136
private static array $activeChannels = [];
135137
private static int $totalActiveChannels = 0;
@@ -202,6 +204,7 @@ public function __construct(array $options = [])
202204
'queryOptions' => [],
203205
'directedReadOptions' => [],
204206
'isolationLevel' => IsolationLevel::ISOLATION_LEVEL_UNSPECIFIED,
207+
'readLockMode' => ReadLockMode::READ_LOCK_MODE_UNSPECIFIED,
205208
'routeToLeader' => true,
206209
'cacheItemPool' => null
207210
];
@@ -211,6 +214,7 @@ public function __construct(array $options = [])
211214
$this->routeToLeader = $options['routeToLeader'];
212215
$this->defaultQueryOptions = $options['queryOptions'];
213216
$this->isolationLevel = $options['isolationLevel'];
217+
$this->readLockMode = $options['readLockMode'];
214218

215219
$options = $this->configureKeepAlive($options);
216220

@@ -589,6 +593,7 @@ public function instance(string $name, array $instance = []): Instance
589593
'defaultQueryOptions' => $this->defaultQueryOptions,
590594
'returnInt64AsObject' => $this->returnInt64AsObject,
591595
'isolationLevel' => $this->isolationLevel,
596+
'readLockMode' => $this->readLockMode,
592597
'cacheItemPool' => $this->cacheItemPool,
593598
'instance' => $instance,
594599
],

Spanner/tests/Unit/DatabaseTest.php

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2378,6 +2378,99 @@ function (Transaction $t) use ($sql) {
23782378
);
23792379
}
23802380

2381+
public function testRunTransactionWithClientLevelIsolationLevel()
2382+
{
2383+
$sql = 'SELECT example FROM sql_query';
2384+
$stream = $this->prophesize(ServerStream::class);
2385+
$stream->readAll()
2386+
->shouldBeCalledOnce()
2387+
->willReturn([new ResultSet(['stats' => new ResultSetStats(['row_count_exact' => 0])])]);
2388+
2389+
$this->spannerClient->executeStreamingSql(
2390+
Argument::that(function (ExecuteSqlRequest $request) {
2391+
$txnOptions = $request->getTransaction()->getBegin();
2392+
$this->assertNotNull($txnOptions);
2393+
$this->assertEquals(IsolationLevel::SERIALIZABLE, $txnOptions->getIsolationLevel());
2394+
return true;
2395+
}),
2396+
Argument::type('array')
2397+
)
2398+
->shouldBeCalledOnce()
2399+
->willReturn($stream->reveal());
2400+
2401+
$session = $this->prophesize(SessionCache::class);
2402+
$session->name()->willReturn($this->sessionName);
2403+
2404+
$database = new Database(
2405+
$this->spannerClient->reveal(),
2406+
$this->databaseAdminClient->reveal(),
2407+
$this->serializer,
2408+
$this->instance,
2409+
self::PROJECT,
2410+
self::DATABASE,
2411+
$session->reveal(),
2412+
['isolationLevel' => IsolationLevel::SERIALIZABLE]
2413+
);
2414+
2415+
$database->runTransaction(
2416+
function (Transaction $t) use ($sql) {
2417+
// Run a fake query
2418+
$t->executeUpdate($sql);
2419+
2420+
// Simulate calling Transaction::commmit()
2421+
$prop = new \ReflectionProperty($t, 'state');
2422+
$prop->setValue($t, Transaction::STATE_COMMITTED);
2423+
}
2424+
);
2425+
}
2426+
2427+
public function testRunTransactionWithClientLevelIsolationLevelOverride()
2428+
{
2429+
$sql = 'SELECT example FROM sql_query';
2430+
$stream = $this->prophesize(ServerStream::class);
2431+
$stream->readAll()
2432+
->shouldBeCalledOnce()
2433+
->willReturn([new ResultSet(['stats' => new ResultSetStats(['row_count_exact' => 0])])]);
2434+
2435+
$this->spannerClient->executeStreamingSql(
2436+
Argument::that(function (ExecuteSqlRequest $request) {
2437+
$txnOptions = $request->getTransaction()->getBegin();
2438+
$this->assertNotNull($txnOptions);
2439+
$this->assertEquals(IsolationLevel::REPEATABLE_READ, $txnOptions->getIsolationLevel());
2440+
return true;
2441+
}),
2442+
Argument::type('array')
2443+
)
2444+
->shouldBeCalledOnce()
2445+
->willReturn($stream->reveal());
2446+
2447+
$session = $this->prophesize(SessionCache::class);
2448+
$session->name()->willReturn($this->sessionName);
2449+
2450+
$database = new Database(
2451+
$this->spannerClient->reveal(),
2452+
$this->databaseAdminClient->reveal(),
2453+
$this->serializer,
2454+
$this->instance,
2455+
self::PROJECT,
2456+
self::DATABASE,
2457+
$session->reveal(),
2458+
['isolationLevel' => IsolationLevel::SERIALIZABLE]
2459+
);
2460+
2461+
$database->runTransaction(
2462+
function (Transaction $t) use ($sql) {
2463+
// Run a fake query
2464+
$t->executeUpdate($sql);
2465+
2466+
// Simulate calling Transaction::commmit()
2467+
$prop = new \ReflectionProperty($t, 'state');
2468+
$prop->setValue($t, Transaction::STATE_COMMITTED);
2469+
},
2470+
['transactionOptions' => ['isolationLevel' => IsolationLevel::REPEATABLE_READ]]
2471+
);
2472+
}
2473+
23812474
public function testRunTransactionWithReadLockMode()
23822475
{
23832476
$sql = 'SELECT example FROM sql_query';
@@ -2415,6 +2508,101 @@ function (Transaction $t) use ($sql) {
24152508
);
24162509
}
24172510

2511+
public function testRunTransactionWithClientLevelReadLockMode()
2512+
{
2513+
$sql = 'SELECT example FROM sql_query';
2514+
$stream = $this->prophesize(ServerStream::class);
2515+
$stream->readAll()
2516+
->shouldBeCalledOnce()
2517+
->willReturn([new ResultSet(['stats' => new ResultSetStats(['row_count_exact' => 0])])]);
2518+
2519+
$this->spannerClient->executeStreamingSql(
2520+
Argument::that(function (ExecuteSqlRequest $request) {
2521+
$txnOptions = $request->getTransaction()->getBegin();
2522+
$this->assertNotNull($txnOptions);
2523+
$this->assertNotNull($readWriteTxnOptions = $txnOptions->getReadWrite());
2524+
$this->assertEquals(ReadLockMode::PESSIMISTIC, $readWriteTxnOptions->getReadLockMode());
2525+
return true;
2526+
}),
2527+
Argument::type('array')
2528+
)
2529+
->shouldBeCalledOnce()
2530+
->willReturn($stream->reveal());
2531+
2532+
$session = $this->prophesize(SessionCache::class);
2533+
$session->name()->willReturn($this->sessionName);
2534+
2535+
$database = new Database(
2536+
$this->spannerClient->reveal(),
2537+
$this->databaseAdminClient->reveal(),
2538+
$this->serializer,
2539+
$this->instance,
2540+
self::PROJECT,
2541+
self::DATABASE,
2542+
$session->reveal(),
2543+
['readLockMode' => ReadLockMode::PESSIMISTIC]
2544+
);
2545+
2546+
$database->runTransaction(
2547+
function (Transaction $t) use ($sql) {
2548+
// Run a fake query
2549+
$t->executeUpdate($sql);
2550+
2551+
// Simulate calling Transaction::commmit()
2552+
$prop = new \ReflectionProperty($t, 'state');
2553+
$prop->setValue($t, Transaction::STATE_COMMITTED);
2554+
}
2555+
);
2556+
}
2557+
2558+
public function testRunTransactionWithClientLevelReadLockModeOverride()
2559+
{
2560+
$sql = 'SELECT example FROM sql_query';
2561+
$stream = $this->prophesize(ServerStream::class);
2562+
$stream->readAll()
2563+
->shouldBeCalledOnce()
2564+
->willReturn([new ResultSet(['stats' => new ResultSetStats(['row_count_exact' => 0])])]);
2565+
2566+
$this->spannerClient->executeStreamingSql(
2567+
Argument::that(function (ExecuteSqlRequest $request) {
2568+
$txnOptions = $request->getTransaction()->getBegin();
2569+
$this->assertNotNull($txnOptions);
2570+
$this->assertNotNull($readWriteTxnOptions = $txnOptions->getReadWrite());
2571+
$this->assertEquals(ReadLockMode::OPTIMISTIC, $readWriteTxnOptions->getReadLockMode());
2572+
return true;
2573+
}),
2574+
Argument::type('array')
2575+
)
2576+
->shouldBeCalledOnce()
2577+
->willReturn($stream->reveal());
2578+
2579+
$session = $this->prophesize(SessionCache::class);
2580+
$session->name()->willReturn($this->sessionName);
2581+
2582+
$database = new Database(
2583+
$this->spannerClient->reveal(),
2584+
$this->databaseAdminClient->reveal(),
2585+
$this->serializer,
2586+
$this->instance,
2587+
self::PROJECT,
2588+
self::DATABASE,
2589+
$session->reveal(),
2590+
['readLockMode' => ReadLockMode::PESSIMISTIC]
2591+
);
2592+
2593+
$database->runTransaction(
2594+
function (Transaction $t) use ($sql) {
2595+
// Run a fake query
2596+
$t->executeUpdate($sql);
2597+
2598+
// Simulate calling Transaction::commmit()
2599+
$prop = new \ReflectionProperty($t, 'state');
2600+
$prop->setValue($t, Transaction::STATE_COMMITTED);
2601+
},
2602+
['transactionOptions' => ['readLockMode' => ReadLockMode::OPTIMISTIC]]
2603+
);
2604+
}
2605+
24182606
public function testTransactionWithReadLockMode()
24192607
{
24202608
$this->spannerClient->beginTransaction(

0 commit comments

Comments
 (0)