Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/emulator-system-tests-spanner.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ jobs:
php-version: '8.1'
ini-values: grpc.enable_fork_support=1
tools: pecl
extensions: bcmath, grpc
extensions: bcmath, grpc, pcntl

- name: Install dependencies
run: |
Expand Down
2 changes: 2 additions & 0 deletions Core/src/Testing/Snippet/SnippetTestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@
*/
class SnippetTestCase extends TestCase
{
const PROJECT = 'my-awesome-project';

use CheckForClassTrait;

private static $coverage;
Expand Down
8 changes: 8 additions & 0 deletions Core/src/Testing/System/SystemTestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
use Google\Cloud\Storage\StorageClient;
use Google\Cloud\Core\Testing\System\DeletionQueue;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Cache\Adapter\FilesystemAdapter;

/**
* SystemTestCase can be extended to implement system tests
Expand Down Expand Up @@ -286,4 +287,11 @@ public static function skipIfEmulatorUsed($reason = null)
self::markTestSkipped($reason ?: 'This test is not supported by the emulator.');
}
}

protected static function getCacheItemPool()
{
return new FilesystemAdapter(
directory: __DIR__ . '/../../../../.cache'
);
}
}
7 changes: 4 additions & 3 deletions Core/tests/Unit/Batch/OpisClosureSerializerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,13 @@
/**
* @group core
* @group batch
* @runTestsInSeparateProcesses
*/
class OpisClosureSerializerTest extends TestCase
{
public function testWrapAndUnwrapClosures()
{
if (!method_exists(SerializableClosure::class, 'enterContext')) {
if (!@method_exists(SerializableClosure::class, 'enterContext')) {
$this->markTestSkipped('Requires ops/serializer:v3');
}

Expand All @@ -49,8 +50,8 @@ public function testWrapAndUnwrapClosures()

public function testWrapAndUnwrapClosuresV4()
{
if (!function_exists('Opis\Closure\serialize')) {
$this->markTestSkipped('Requires ops/serializer:v3');
if (@method_exists(SerializableClosure::class, 'enterContext')) {
$this->markTestSkipped('Requires ops/serializer:v4');
}

$data['closure'] = function () {
Expand Down
1 change: 1 addition & 0 deletions Core/tests/Unit/Lock/FlockLockTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
/**
* @group core
* @group lock
* @runTestsInSeparateProcesses
*/
class FlockLockTest extends TestCase
{
Expand Down
1 change: 1 addition & 0 deletions Core/tests/Unit/Lock/SemaphoreLockTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
/**
* @group core
* @group lock
* @runTestsInSeparateProcesses
*/
class SemaphoreLockTest extends TestCase
{
Expand Down
2 changes: 1 addition & 1 deletion Datastore/tests/Snippet/FilterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ class FilterTest extends SnippetTestCase
use ProphecyTrait;
use ProtoEncodeTrait;

private const PROJECT = 'alpha-project';
const PROJECT = 'alpha-project';
private $gapicClient;
private $datastore;
private $operation;
Expand Down
2 changes: 1 addition & 1 deletion PubSub/tests/Snippet/PubSubClientTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ class PubSubClientTest extends SnippetTestCase
{
use ProphecyTrait;

private const PROJECT_ID = 'my-awesome-project';
const PROJECT_ID = 'my-awesome-project';
private const TOPIC = 'projects/my-awesome-project/topics/my-new-topic';
private const SUBSCRIPTION = 'projects/my-awesome-project/subscriptions/my-new-subscription';
private const SNAPSHOT = 'projects/my-awesome-project/snapshots/my-snapshot';
Expand Down
2 changes: 1 addition & 1 deletion PubSub/tests/Snippet/SnapshotTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ class SnapshotTest extends SnippetTestCase
use ProphecyTrait;
use ApiHelperTrait;

private const PROJECT = 'my-awesome-project';
const PROJECT = 'my-awesome-project';
private const SNAPSHOT = 'projects/my-awesome-project/snapshots/my-snapshot';
private const PROJECT_ID = 'my-awesome-project';

Expand Down
18 changes: 17 additions & 1 deletion Spanner/MIGRATING.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,4 +117,20 @@ $lro->delete();
### Removed Methods

- `Operation::createTransaction` => use `Operation::transaction` instead
- `Operation::createSnapshot` => use `Operation::snapshot` instead
- `Operation::createSnapshot` => use `Operation::snapshot` instead
- `Database::close` => obsolete
- `Database::sessionPool` => obsolete
- `Database::batchCreateSessions` => obsolete
- `Database::deleteSessionAsync` => obsolete
- `BatchSnapshot::close` => obsolete
- `Operation::session` => obsolete
- `Operation::createSession` => (obsolete)
- `Operation::commitWithResponse` (obsolete) => use `Operation::commit` instead

### Removed Classes

- `Session\Session` => removed in favor of `SessionCache`
- `Session\CacheSessionPool` => removed in favor of `SessionCache`
- `Session\SessionPoolInterface` => removed in favor of `SessionCache`
- `Operation` - this class is marked `@internal`, and should not be used directly.

141 changes: 125 additions & 16 deletions Spanner/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,29 +34,138 @@ on authenticating your client. Once authenticated, you'll be ready to start maki
### Sample

```php
use Google\ApiCore\ApiException;
use Google\Cloud\Spanner\V1\Client\SpannerClient;
use Google\Cloud\Spanner\V1\GetSessionRequest;
use Google\Cloud\Spanner\V1\Session;
use Google\Cloud\Spanner\SpannerClient;

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

// Prepare the request message.
$request = (new GetSessionRequest())
->setName($formattedName);

// Call the API and handle any network failures.
try {
/** @var Session $response */
$response = $spannerClient->getSession($request);
printf('Response data: %s' . PHP_EOL, $response->serializeToJsonString());
} catch (ApiException $ex) {
printf('Call failed with message: %s' . PHP_EOL, $ex->getMessage());
$db = $spanner->connect('my-instance', 'my-database');

$userQuery = $db->execute('SELECT * FROM Users WHERE id = @id', [
'parameters' => [
'id' => $userId
]
]);

$user = $userQuery->rows()->current();

echo 'Hello ' . $user['firstName'];
```

### Multiplexed Sessions

The V2 version of the Spanner Client Library for PHP uses [Multiplexed Sessions][mux-sessions]. Multiplexed Sessions
allow your application to create a large number of concurrent requests on a single session. Some advantages include
reduced backend resource consumption due to a more straightforward session management protocol, and less management
as sessions no longer require cleanup after use or keep-alive requests when idle.

#### Session Caching

The session cache is configured with a default cache which uses the PSR-6 compatible [`SysvCacheItemPool`][sysv-cache]
when the [`sysvshm`][sysvshm] extension is enabled, and [`FileSystemCacheItemPool`][file-cache] when `sysvshm` is not
available. This ensures that your processes share a single multiplex session for each database and creator role.

To change the default cache pool, use the option `cacheItemPool` when instantiating your Spanner client:

```php
use Google\Cloud\Spanner\SpannerClient;
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
// available by running `composer install symfony/cache`
$fileCacheItemPool = new FilesystemAdapter();
// configure through SpannerClient constructor
$spanner = new SpannerClient(['cacheItemPool' => $fileCacheItemPool]);
$database = $spanner->instance($instanceId)->database($databaseId);
```

This can also be passed in as an option to the `instance` or `database` methods:
```php
$spanner = new SpannerClient();
// configure through instance method
$database = $spanner
->instance($instanceId, ['cacheItemPool' => $fileCacheItemPool])
->database($databaseId);
// configure through database method
$database = $spanner
->instance($instanceId)
->database($databaseId, ['cacheItemPool' => $fileCacheItemPool]);
```

[sysvshm]: https://www.php.net/manual/en/book.sem.php
[file-cache]: https://github.com/googleapis/google-auth-library-php/blob/main/src/Cache/FileSystemCacheItemPool.php
[sysv-cache]: https://github.com/googleapis/google-auth-library-php/blob/main/src/Cache/SysVCacheItemPool.php

#### Refreshing Sessions

Sessions will refresh synchronously every 7 days. You can use this script to refresh the session asynchronously, in
to avoid latency in your application (recommended every ~24 hours):

```php
// If you are using a custom PSR-6 cache via the "cacheItemPool" client option in your
// application, you will need to supply a cache with the same configuration here in
// order to properly refresh the session.
$spanner = new SpannerClient();

$sessionCache = $spanner
->instance($instanceId)
->database($databaseId)
->session();

// this will force-refresh the session
$sessionCache->refresh();
```

[mux-sessions]: https://cloud.google.com/spanner/docs/sessions#multiplexed_sessions

#### Session Locking

Locking occurs when a new session is created, and ensures no race conditions occur when a session expires.
Locking uses a [`Semaphore`][sem-lock] lock when `sysvmsg`, `sysvsem`, and `sysvshm` extensions are enabled, and a
[`Flock`][flock-lock] lock otherwise. To configure a custom lock, supply a class implementing
[`LockInterface`][lock-interface] when calling `Instance::database`. Here's an example which encorporates the
[Symfony Lock component][symfony-lock]:

```php
use Google\Cloud\Core\Lock\LockInterface;
use Google\Cloud\Spanner\SpannerClient;
use Symfony\Component\Lock\LockFactory;
use Symfony\Component\Lock\SharedLockInterface;
use Symfony\Component\Lock\Store\SemaphoreStore;

// Available by running `composer install symfony/lock`
$store = new SemaphoreStore();
$factory = new LockFactory($store);

// Create an adapter for Symfony's SharedLockInterface and Google's LockInterface
$lock = new class ($factory->createLock($databaseId)) implements LockInterface {
public function __construct(private SharedLockInterface $lock) {
}

public function acquire(array $options = []) {
return $this->lock->acquire()
}

public function release() {
return $this->lock->acquire()
}

public function synchronize(callable $func, array $options = []) {
if ($this->lock->acquire($options['blocking'] ?? true)) {
return $func();
}
}
}

// Configure our custom lock on our database using the "lock" option
$spanner = new SpannerClient();
$database = $spanner
->instance($instanceId)
->database($databaseId, ['lock' => $lock]);
```

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.
[sem-lock]: https://github.com/googleapis/google-cloud-php/blob/main/Core/src/Lock/SemaphoreLock.php
[flock-lock]: https://github.com/googleapis/google-cloud-php/blob/main/Core/src/Lock/FlockLock.php
[lock-interface]: https://github.com/googleapis/google-cloud-php/blob/main/Core/src/Lock/LockInterface.php
[symfony-lock]: https://symfony.com/doc/current/components/lock.html

### Debugging

Expand Down
27 changes: 23 additions & 4 deletions Spanner/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,17 @@
"google/gax": "^1.38.0"
},
"require-dev": {
"phpunit/phpunit": "^9.0",
"phpspec/prophecy-phpunit": "^2.0",
"squizlabs/php_codesniffer": "2.*",
"phpunit/phpunit": "^9.6",
"phpspec/prophecy-phpunit": "^2.1",
"squizlabs/php_codesniffer": "3.*",
"phpdocumentor/reflection": "^5.3.3||^6.0",
"phpdocumentor/reflection-docblock": "^5.3",
"erusev/parsedown": "^1.6",
"google/cloud-pubsub": "^2.0",
"dg/bypass-finals": "^1.7",
"dms/phpunit-arraysubset-asserts": "^0.5.0"
"dms/phpunit-arraysubset-asserts": "^0.5.0",
"symfony/cache": "^6.4",
"symfony/process": "^6.4"
},
"suggest": {
"ext-protobuf": "Provides a significant increase in throughput over the pure PHP protobuf implementation. See https://cloud.google.com/php/grpc for installation instructions.",
Expand All @@ -44,5 +46,22 @@
"Testing\\Data\\": "tests/data/generated/Testing/Data",
"GPBMetadata\\Data\\": "tests/data/generated/GPBMetadata/Data"
}
},
"scripts": {
"test-unit": [
"vendor/bin/phpunit --testdox --stop-on-failure"
],
"test-snippets": [
"vendor/bin/phpunit -c phpunit-snippets.xml.dist --testdox --stop-on-failure"
],
"test-system": [
"Composer\\Config::disableProcessTimeout",
"vendor/bin/phpunit -c phpunit-system.xml.dist --testdox --stop-on-failure"
],
"test-all": [
"@test-unit",
"@test-snippets",
"@test-system"
]
}
}
17 changes: 7 additions & 10 deletions Spanner/src/Backup.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
use DateTimeInterface;
use Google\ApiCore\Options\CallOptions;
use Google\ApiCore\ValidationException;
use Google\Cloud\Core\ApiHelperTrait;
use Google\Cloud\Core\Exception\NotFoundException;
use Google\Cloud\Core\Iterator\ItemIterator;
use Google\Cloud\Core\LongRunning\LongRunningClientConnection;
Expand Down Expand Up @@ -51,6 +52,7 @@
class Backup
{
use RequestTrait;
use ApiHelperTrait;

const STATE_READY = State::READY;
const STATE_CREATING = State::CREATING;
Expand Down Expand Up @@ -372,22 +374,17 @@ public function state(array $options = []): int|null
*/
public function updateExpireTime(DateTimeInterface $newTimestamp, array $options = []): array
{
$options += [
'backup' => [
'name' => $this->name(),
'expireTime' => $this->formatTimeAsArray($newTimestamp),
],
'updateMask' => [
'paths' => ['expire_time']
]
];
$options['expireTime'] = $this->formatTimeAsArray($newTimestamp);

/**
* @var UpdateBackupRequest $updateBackup
* @var array $callOptions
*/
[$updateBackup, $callOptions] = $this->validateOptions(
$options,
[
'backup' => $options + ['name' => $this->name()],
'updateMask' => $this->fieldMask($options),
],
new UpdateBackupRequest(),
CallOptions::class,
);
Expand Down
Loading
Loading