Skip to content

Commit ce8b785

Browse files
author
Bertrand Dunogier
committed
[schema sync] Implemented (optional) AWS S3 sync
1 parent 573e03e commit ce8b785

File tree

7 files changed

+201
-8
lines changed

7 files changed

+201
-8
lines changed

composer.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
"ezsystems/ezplatform-admin-ui": "^2.0@dev",
2020
"ezsystems/ezplatform-rest": "^1.2@dev",
2121
"ezsystems/ezplatform-richtext": "^2.0@dev",
22-
"ext-redis": "*",
2322
"lexik/jwt-authentication-bundle": "^2.8",
2423
"overblog/graphql-bundle": "^0.12",
2524
"erusev/parsedown": "^1.7",
@@ -44,6 +43,10 @@
4443
"ezsystems/ezplatform-code-style": "^0.1.0",
4544
"mikey179/vfsstream": "^1.6"
4645
},
46+
"suggest": {
47+
"aws/aws-sdk-php": "For compiled schema synchronization over AWS-S3",
48+
"ext-redis": "*"
49+
},
4750
"autoload": {
4851
"psr-4": {
4952
"EzSystems\\EzPlatformGraphQL\\": "src",

src/DependencyInjection/EzSystemsEzPlatformGraphQLExtension.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
*/
77
namespace EzSystems\EzPlatformGraphQL\DependencyInjection;
88

9+
use Aws\S3\S3Client;
910
use EzSystems\EzPlatformGraphQL\DependencyInjection\GraphQL\YamlSchemaProvider;
1011
use Symfony\Component\Config\FileLocator;
1112
use Symfony\Component\DependencyInjection\ContainerBuilder;
@@ -42,6 +43,13 @@ public function load(array $configs, ContainerBuilder $container)
4243
$loader->load('services/schema_sync.yaml');
4344
$loader->load('services/services.yaml');
4445
$loader->load('default_settings.yaml');
46+
47+
if (extension_loaded('redis')) {
48+
$loader->load('services/schema_sync.yaml');
49+
if (class_exists(S3Client::class)) {
50+
$loader->load('services/schema_sync_s3.yaml');
51+
}
52+
}
4553
}
4654

4755
/**

src/Resources/config/services/schema_sync.yaml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ services:
77
autoconfigure: true
88
autowire: true
99
public: false
10+
bind:
11+
EzSystems\EzPlatformGraphQL\Schema\Sync\TimestampHandler: '@EzSystems\EzPlatformGraphQL\Schema\Sync\RedisTimestampHandler'
12+
Redis $graphQLSyncRedis: '@ibexa_graphql.sync.redis_client'
1013

1114
EzSystems\EzPlatformGraphQL\Command\PublishSchemaCommand:
1215
arguments:
@@ -26,14 +29,11 @@ services:
2629
arguments:
2730
$definitionsDirectory: '%ibexa_graphql.definitions_directory%'
2831

29-
ibexa_graphql.redis:
32+
ibexa_graphql.sync.redis_client:
3033
class: Redis
3134
calls:
3235
- connect: ['%env(REDIS_GRAPHQL_HOST)%', '%env(REDIS_GRAPHQL_PORT)%']
3336
- select: ['%env(REDIS_GRAPHQL_DBINDEX)%']
3437

35-
EzSystems\EzPlatformGraphQL\Schema\Sync\TimestampHandler: '@EzSystems\EzPlatformGraphQL\Schema\Sync\RedisTimestampHandler'
38+
EzSystems\EzPlatformGraphQL\Schema\Sync\RedisTimestampHandler: ~
3639

37-
EzSystems\EzPlatformGraphQL\Schema\Sync\RedisTimestampHandler:
38-
arguments:
39-
$redis: '@ibexa_graphql.redis'
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
services:
2+
_defaults:
3+
bind:
4+
Aws\S3\S3Client $graphQLSyncS3Client: '@ibexa_graphql.sync.s3_client'
5+
EzSystems\EzPlatformGraphQL\Schema\Sync\TimestampHandler: '@EzSystems\EzPlatformGraphQL\Schema\Sync\RedisTimestampHandler'
6+
7+
ibexa_graphql.sync.s3_client:
8+
class: Aws\S3\S3Client
9+
arguments:
10+
- version: '2006-03-01'
11+
region: '%env(GRAPHQL_SYNC_S3_REGION)%'
12+
13+
EzSystems\EzPlatformGraphQL\Schema\Sync\S3SharedSchema:
14+
arguments:
15+
$bucket: '%env(GRAPHQL_SYNC_S3_BUCKET)%'
16+
17+
EzSystems\EzPlatformGraphQL\Schema\Sync\SharedSchema: '@EzSystems\EzPlatformGraphQL\Schema\Sync\S3SharedSchema'

src/Schema/Sync/README.md

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# GraphQL schema sync
2+
3+
An experimental mechanism for publishing a compiled schema so that secondary servers can pull it and install it without recompiling their own containe.
4+
5+
## Configuration
6+
7+
### Redis
8+
The feature requires Redis for publishing the latest schema timestamp (it is suggested in `composer.json`).
9+
10+
The feature comes with a default redis client service, `ibexa_graphql.sync.redis_client`.
11+
It is configured using the following environment variables:
12+
```.dotenv
13+
REDIS_GRAPHQL_HOST=1.2.3.4
14+
REDIS_GRAPHQL_PORT=6379
15+
REDIS_GRAPHQL_DBINDEX=0
16+
```
17+
18+
If you want to use your own client, you can redefined the service with the same name in your
19+
project's services definitions.
20+
21+
If you already have your own and want to re-use it, create an alias with that name:
22+
```yaml
23+
# config/services.yaml
24+
services:
25+
ibexa_graphql.sync.redis_client: '@app.redis_client'
26+
```
27+
28+
### AWS S3
29+
Amazon S3 can be used to publish the schema files. To enable it, make sure that `aws/aws-sdk-php`
30+
is installed on your project.
31+
32+
It uses a default client service named `ibexa_graphql.sync.s3_client`, based on the default
33+
environment variables expected by the SDK:
34+
35+
```dotenv
36+
AWS_ACCESS_KEY_ID=
37+
AWS_SECRET_ACCESS_KEY=
38+
```
39+
40+
If you already have an s3 client service, alias it to `ibexa_graphql.sync.s3_client`:
41+
```yaml
42+
# config/services.yaml
43+
services:
44+
ibexa_graphql.sync.s3_client: '@app.s3_client'
45+
```
46+
47+
The feature also requires two extra settings for the bucket and the region:
48+
```dotenv
49+
GRAPHQL_SYNC_S3_BUCKET=ibexa-graphql
50+
GRAPHQL_SYNC_S3_REGION=eu-west-1
51+
```
52+
53+
## Usage
54+
One server will do the schema generation + compiling, and run a command
55+
(`ibexa:graphql:publish-schema`) to publish the schema. The command can be executed during a deployment process,
56+
or manually.
57+
58+
Publishing will:
59+
60+
1. push the compiled schema (`%kernel.cache_dir%/overblog/graphql-bundle/__definitions__/*`) to a SharedSchema
61+
2. set the published schema timestamp using a TimestampHandler.
62+
63+
Secondary servers, when a GraphQL query is executed (`UpdateSchemaIfNeeded` subscriber), will compare the timestamp to
64+
theirs (modification time of `__classes.map`). If the remote schema is newer, it will be pulled and installed on shutdown.
65+
66+
Since the graphql schema types are compiled into the container as services, types that were added to the published schema
67+
(new content types, etc) need to be registered on runtime. This is done by the `Schema\Sync\AddTypesSolutions` subscriber.
68+
It checks which of the type classes do not have a solution in the current schema, and adds them to it.

src/Schema/Sync/RedisTimestampHandler.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,9 @@ class RedisTimestampHandler implements TimestampHandler
2020
*/
2121
private $key;
2222

23-
public function __construct(Redis $redis, string $key = 'graphql_schema_timestamp')
23+
public function __construct(Redis $graphQLSyncRedis, string $key = 'graphql_schema_timestamp')
2424
{
25-
$this->redis = $redis;
25+
$this->redis = $graphQLSyncRedis;
2626
$this->key = $key;
2727
}
2828

src/Schema/Sync/S3SharedSchema.php

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
<?php
2+
/**
3+
* @copyright Copyright (C) eZ Systems AS. All rights reserved.
4+
* @license For full copyright and license information view LICENSE file distributed with this source code.
5+
*/
6+
7+
namespace EzSystems\EzPlatformGraphQL\Schema\Sync;
8+
9+
use Aws\Result;
10+
use Aws\S3\Exception\S3Exception;
11+
use Aws\S3\S3Client;
12+
use EzSystems\EzPlatformGraphQL\Schema\Sync\SharedSchema;
13+
use EzSystems\EzPlatformGraphQL\Schema\Sync\TimestampHandler;
14+
use Symfony\Component\Finder\Finder;
15+
16+
class S3SharedSchema implements SharedSchema
17+
{
18+
/**
19+
* @var array Map of filename => file contents
20+
*/
21+
private $files = [];
22+
23+
/**
24+
* @var \EzSystems\EzPlatformGraphQL\Schema\Sync\TimestampHandler
25+
*/
26+
private $timestampHandler;
27+
/**
28+
* @var \Aws\S3\S3Client
29+
*/
30+
private $s3;
31+
32+
private $bucket;
33+
34+
public function __construct(TimestampHandler $timestampHandler, S3Client $graphQLSyncS3Client, string $bucket)
35+
{
36+
$this->timestampHandler = $timestampHandler;
37+
$this->s3 = $graphQLSyncS3Client;
38+
$this->bucket = $bucket;
39+
}
40+
41+
public function addFile(string $name, string $contents)
42+
{
43+
$this->files[$name] = $contents;
44+
}
45+
46+
public function publish(int $timestamp)
47+
{
48+
foreach ($this->files as $name => $contents) {
49+
$this->putFileToS3("$timestamp/$name", $contents);
50+
}
51+
52+
$this->timestampHandler->set($timestamp);
53+
}
54+
55+
public function getFiles(int $timestamp): array
56+
{
57+
if (!$this->hasFileOnS3("$timestamp/__classes.map")) {
58+
throw new \Exception("Shared schema not found");
59+
}
60+
61+
$files = [];
62+
63+
$prefix = "$timestamp/";
64+
$listResult = $this->s3->listObjectsV2(['Bucket' => $this->bucket, 'Prefix' => $prefix]);
65+
foreach ($listResult->get('Contents') as $listItem) {
66+
$fileResult = $this->s3->getObject(['Bucket' => $this->bucket, 'Key' => $listItem['Key']]);
67+
$fileName = str_replace($prefix, '', $listItem['Key']);
68+
$files[$fileName] = $fileResult->get('Body')->getContents();
69+
}
70+
71+
return $files;
72+
}
73+
74+
private function putFileToS3(string $name, string $contents): void
75+
{
76+
try {
77+
$this->s3->putObject([
78+
'Bucket' => $this->bucket,
79+
'Key' => $name,
80+
'Body' => $contents,
81+
]);
82+
} catch (S3Exception $e) {
83+
throw new \Exception("Error creating file", 0, $e);
84+
}
85+
}
86+
87+
private function hasFileOnS3(string $path): bool
88+
{
89+
try {
90+
$this->s3->getObject(['Bucket' => $this->bucket, 'Key' => $path]);
91+
} catch (S3Exception $e) {
92+
return false;
93+
}
94+
95+
return true;
96+
}
97+
}

0 commit comments

Comments
 (0)