Skip to content

Commit 9e5f26a

Browse files
authored
feat(Spanner): multiplexed sessions (#8666)
1 parent b67a2c3 commit 9e5f26a

62 files changed

Lines changed: 1561 additions & 4180 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/emulator-system-tests-spanner.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ jobs:
4444
php-version: '8.1'
4545
ini-values: grpc.enable_fork_support=1
4646
tools: pecl
47-
extensions: bcmath, grpc
47+
extensions: bcmath, grpc, pcntl
4848

4949
- name: Install dependencies
5050
run: |

Core/src/Testing/System/SystemTestCase.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
use Google\Cloud\Storage\StorageClient;
2929
use Google\Cloud\Core\Testing\System\DeletionQueue;
3030
use PHPUnit\Framework\TestCase;
31+
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
3132

3233
/**
3334
* SystemTestCase can be extended to implement system tests
@@ -286,4 +287,11 @@ public static function skipIfEmulatorUsed($reason = null)
286287
self::markTestSkipped($reason ?: 'This test is not supported by the emulator.');
287288
}
288289
}
290+
291+
protected static function getCacheItemPool()
292+
{
293+
return new FilesystemAdapter(
294+
directory: __DIR__ . '/../../../../.cache'
295+
);
296+
}
289297
}

Spanner/MIGRATING.md

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,4 +117,20 @@ $lro->delete();
117117
### Removed Methods
118118

119119
- `Operation::createTransaction` => use `Operation::transaction` instead
120-
- `Operation::createSnapshot` => use `Operation::snapshot` instead
120+
- `Operation::createSnapshot` => use `Operation::snapshot` instead
121+
- `Database::close` => obsolete
122+
- `Database::sessionPool` => obsolete
123+
- `Database::batchCreateSessions` => obsolete
124+
- `Database::deleteSessionAsync` => obsolete
125+
- `BatchSnapshot::close` => obsolete
126+
- `Operation::session` => obsolete
127+
- `Operation::createSession` => (obsolete)
128+
- `Operation::commitWithResponse` (obsolete) => use `Operation::commit` instead
129+
130+
### Removed Classes
131+
132+
- `Session\Session` => removed in favor of `SessionCache`
133+
- `Session\CacheSessionPool` => removed in favor of `SessionCache`
134+
- `Session\SessionPoolInterface` => removed in favor of `SessionCache`
135+
- `Operation` - this class is marked `@internal`, and should not be used directly.
136+

Spanner/README.md

Lines changed: 125 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -34,29 +34,138 @@ on authenticating your client. Once authenticated, you'll be ready to start maki
3434
### Sample
3535

3636
```php
37-
use Google\ApiCore\ApiException;
38-
use Google\Cloud\Spanner\V1\Client\SpannerClient;
39-
use Google\Cloud\Spanner\V1\GetSessionRequest;
40-
use Google\Cloud\Spanner\V1\Session;
37+
use Google\Cloud\Spanner\SpannerClient;
4138

4239
// Create a client.
4340
$spannerClient = new SpannerClient();
4441

45-
// Prepare the request message.
46-
$request = (new GetSessionRequest())
47-
->setName($formattedName);
48-
49-
// Call the API and handle any network failures.
50-
try {
51-
/** @var Session $response */
52-
$response = $spannerClient->getSession($request);
53-
printf('Response data: %s' . PHP_EOL, $response->serializeToJsonString());
54-
} catch (ApiException $ex) {
55-
printf('Call failed with message: %s' . PHP_EOL, $ex->getMessage());
42+
$db = $spanner->connect('my-instance', 'my-database');
43+
44+
$userQuery = $db->execute('SELECT * FROM Users WHERE id = @id', [
45+
'parameters' => [
46+
'id' => $userId
47+
]
48+
]);
49+
50+
$user = $userQuery->rows()->current();
51+
52+
echo 'Hello ' . $user['firstName'];
53+
```
54+
55+
### Multiplexed Sessions
56+
57+
The V2 version of the Spanner Client Library for PHP uses [Multiplexed Sessions][mux-sessions]. Multiplexed Sessions
58+
allow your application to create a large number of concurrent requests on a single session. Some advantages include
59+
reduced backend resource consumption due to a more straightforward session management protocol, and less management
60+
as sessions no longer require cleanup after use or keep-alive requests when idle.
61+
62+
#### Session Caching
63+
64+
The session cache is configured with a default cache which uses the PSR-6 compatible [`SysvCacheItemPool`][sysv-cache]
65+
when the [`sysvshm`][sysvshm] extension is enabled, and [`FileSystemCacheItemPool`][file-cache] when `sysvshm` is not
66+
available. This ensures that your processes share a single multiplex session for each database and creator role.
67+
68+
To change the default cache pool, use the option `cacheItemPool` when instantiating your Spanner client:
69+
70+
```php
71+
use Google\Cloud\Spanner\SpannerClient;
72+
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
73+
// available by running `composer install symfony/cache`
74+
$fileCacheItemPool = new FilesystemAdapter();
75+
// configure through SpannerClient constructor
76+
$spanner = new SpannerClient(['cacheItemPool' => $fileCacheItemPool]);
77+
$database = $spanner->instance($instanceId)->database($databaseId);
78+
```
79+
80+
This can also be passed in as an option to the `instance` or `database` methods:
81+
```php
82+
$spanner = new SpannerClient();
83+
// configure through instance method
84+
$database = $spanner
85+
->instance($instanceId, ['cacheItemPool' => $fileCacheItemPool])
86+
->database($databaseId);
87+
// configure through database method
88+
$database = $spanner
89+
->instance($instanceId)
90+
->database($databaseId, ['cacheItemPool' => $fileCacheItemPool]);
91+
```
92+
93+
[sysvshm]: https://www.php.net/manual/en/book.sem.php
94+
[file-cache]: https://github.com/googleapis/google-auth-library-php/blob/main/src/Cache/FileSystemCacheItemPool.php
95+
[sysv-cache]: https://github.com/googleapis/google-auth-library-php/blob/main/src/Cache/SysVCacheItemPool.php
96+
97+
#### Refreshing Sessions
98+
99+
Sessions will refresh synchronously every 7 days. You can use this script to refresh the session asynchronously, in
100+
to avoid latency in your application (recommended every ~24 hours):
101+
102+
```php
103+
// If you are using a custom PSR-6 cache via the "cacheItemPool" client option in your
104+
// application, you will need to supply a cache with the same configuration here in
105+
// order to properly refresh the session.
106+
$spanner = new SpannerClient();
107+
108+
$sessionCache = $spanner
109+
->instance($instanceId)
110+
->database($databaseId)
111+
->session();
112+
113+
// this will force-refresh the session
114+
$sessionCache->refresh();
115+
```
116+
117+
[mux-sessions]: https://cloud.google.com/spanner/docs/sessions#multiplexed_sessions
118+
119+
#### Session Locking
120+
121+
Locking occurs when a new session is created, and ensures no race conditions occur when a session expires.
122+
Locking uses a [`Semaphore`][sem-lock] lock when `sysvmsg`, `sysvsem`, and `sysvshm` extensions are enabled, and a
123+
[`Flock`][flock-lock] lock otherwise. To configure a custom lock, supply a class implementing
124+
[`LockInterface`][lock-interface] when calling `Instance::database`. Here's an example which encorporates the
125+
[Symfony Lock component][symfony-lock]:
126+
127+
```php
128+
use Google\Cloud\Core\Lock\LockInterface;
129+
use Google\Cloud\Spanner\SpannerClient;
130+
use Symfony\Component\Lock\LockFactory;
131+
use Symfony\Component\Lock\SharedLockInterface;
132+
use Symfony\Component\Lock\Store\SemaphoreStore;
133+
134+
// Available by running `composer install symfony/lock`
135+
$store = new SemaphoreStore();
136+
$factory = new LockFactory($store);
137+
138+
// Create an adapter for Symfony's SharedLockInterface and Google's LockInterface
139+
$lock = new class ($factory->createLock($databaseId)) implements LockInterface {
140+
public function __construct(private SharedLockInterface $lock) {
141+
}
142+
143+
public function acquire(array $options = []) {
144+
return $this->lock->acquire()
145+
}
146+
147+
public function release() {
148+
return $this->lock->acquire()
149+
}
150+
151+
public function synchronize(callable $func, array $options = []) {
152+
if ($this->lock->acquire($options['blocking'] ?? true)) {
153+
return $func();
154+
}
155+
}
56156
}
157+
158+
// Configure our custom lock on our database using the "lock" option
159+
$spanner = new SpannerClient();
160+
$database = $spanner
161+
->instance($instanceId)
162+
->database($databaseId, ['lock' => $lock]);
57163
```
58164

59-
By using a cache implementation like `SysVCacheItemPool`, you can share the cached sessions among multiple processes, so that for example, you can warmup the session upon the server startup, then all the other PHP processes will benefit from the warmed up sessions.
165+
[sem-lock]: https://github.com/googleapis/google-cloud-php/blob/main/Core/src/Lock/SemaphoreLock.php
166+
[flock-lock]: https://github.com/googleapis/google-cloud-php/blob/main/Core/src/Lock/FlockLock.php
167+
[lock-interface]: https://github.com/googleapis/google-cloud-php/blob/main/Core/src/Lock/LockInterface.php
168+
[symfony-lock]: https://symfony.com/doc/current/components/lock.html
60169

61170
### Debugging
62171

Spanner/composer.json

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@
1818
"erusev/parsedown": "^1.6",
1919
"google/cloud-pubsub": "^2.0",
2020
"dg/bypass-finals": "^1.7",
21-
"dms/phpunit-arraysubset-asserts": "^0.5.0"
21+
"dms/phpunit-arraysubset-asserts": "^0.5.0",
22+
"symfony/cache": "^6.4",
23+
"symfony/process": "^6.4"
2224
},
2325
"suggest": {
2426
"ext-protobuf": "Provides a significant increase in throughput over the pure PHP protobuf implementation. See https://cloud.google.com/php/grpc for installation instructions.",
@@ -44,5 +46,22 @@
4446
"Testing\\Data\\": "tests/data/generated/Testing/Data",
4547
"GPBMetadata\\Data\\": "tests/data/generated/GPBMetadata/Data"
4648
}
49+
},
50+
"scripts": {
51+
"test-unit": [
52+
"vendor/bin/phpunit --testdox --stop-on-failure"
53+
],
54+
"test-snippets": [
55+
"vendor/bin/phpunit -c phpunit-snippets.xml.dist --testdox --stop-on-failure"
56+
],
57+
"test-system": [
58+
"Composer\\Config::disableProcessTimeout",
59+
"vendor/bin/phpunit -c phpunit-system.xml.dist --testdox --stop-on-failure"
60+
],
61+
"test-all": [
62+
"@test-unit",
63+
"@test-snippets",
64+
"@test-system"
65+
]
4766
}
4867
}

Spanner/src/Batch/BatchClient.php

Lines changed: 8 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
use Google\Cloud\Core\TimeTrait;
2121
use Google\Cloud\Spanner\Operation;
22+
use Google\Cloud\Spanner\Session\SessionCache;
2223
use Google\Cloud\Spanner\Timestamp;
2324
use Google\Cloud\Spanner\TransactionConfigurationTrait;
2425
use Google\Protobuf\Duration;
@@ -73,10 +74,6 @@
7374
* // and is not implemented here.
7475
* do {
7576
* $finished = areWorkersDone();
76-
*
77-
* if ($finished) {
78-
* $snapshot->close();
79-
* }
8077
* } while(!$finished);
8178
* ```
8279
*
@@ -113,25 +110,14 @@ class BatchClient
113110
ReadPartition::class
114111
];
115112

116-
private Operation $operation;
117-
private string $databaseName;
118-
private string|null $databaseRole;
119-
120113
/**
121114
* @param Operation $operation A Cloud Spanner Operations wrapper.
122-
* @param string $databaseName The database name to which the batch client
123-
* instance is scoped.
124-
* @param array $options [optional] {
125-
* Configuration options.
126-
*
127-
* @type string $databaseRole The user created database role which creates the session.
128-
* }
115+
* @param SessionCache $session The session to which the batch client instance is scoped.
129116
*/
130-
public function __construct(Operation $operation, $databaseName, array $options = [])
131-
{
132-
$this->operation = $operation;
133-
$this->databaseName = $databaseName;
134-
$this->databaseRole = $options['databaseRole'] ?? '';
117+
public function __construct(
118+
private Operation $operation,
119+
private SessionCache $session,
120+
) {
135121
}
136122

137123
/**
@@ -154,7 +140,6 @@ public function __construct(Operation $operation, $databaseName, array $options
154140
* timestamp.
155141
* @type Duration $transactionOptions.exactStaleness Represents a number of seconds. Executes
156142
* all reads at a timestamp that is $exactStaleness old.
157-
* @type array $sessionOptions Configuration options for session creation.
158143
* }
159144
* @return BatchSnapshot
160145
*/
@@ -164,8 +149,6 @@ public function snapshot(array $options = [])
164149
'transactionOptions' => [],
165150
];
166151

167-
$sessionOptions = $this->pluck('sessionOptions', $options, false) ?: [];
168-
169152
// Single Use transactions are not supported in batch mode.
170153
$options['transactionOptions']['singleUse'] = false;
171154

@@ -174,17 +157,8 @@ public function snapshot(array $options = [])
174157

175158
$transactionOptions = $this->configureReadOnlyTransactionOptions($transactionOptions);
176159

177-
if ($this->databaseRole !== null) {
178-
$sessionOptions['creator_role'] = $this->databaseRole;
179-
}
180-
181-
$session = $this->operation->createSession(
182-
$this->databaseName,
183-
$sessionOptions
184-
);
185-
186160
/** @var BatchSnapshot */
187-
return $this->operation->snapshot($session, [
161+
return $this->operation->snapshot($this->session, [
188162
'className' => BatchSnapshot::class,
189163
'transactionOptions' => $transactionOptions
190164
] + $options);
@@ -217,8 +191,6 @@ public function snapshotFromString($identifier)
217191
throw new \InvalidArgumentException('Invalid identifier.');
218192
}
219193

220-
$session = $this->operation->session($data['sessionName']);
221-
222194
if ($data['readTimestamp']) {
223195
if (!($data['readTimestamp'] instanceof Timestamp)) {
224196
$time = $this->parseTimeString($data['readTimestamp']);
@@ -227,7 +199,7 @@ public function snapshotFromString($identifier)
227199
}
228200
return new BatchSnapshot(
229201
$this->operation,
230-
$session,
202+
$this->session,
231203
[
232204
'id' => $data['transactionId'],
233205
'readTimestamp' => $data['readTimestamp']

0 commit comments

Comments
 (0)