Skip to content

Commit e7b55b0

Browse files
committed
Merge branch 'master' into 130-add-php-fpm-dashboard
2 parents fbf2756 + b6f7fc1 commit e7b55b0

26 files changed

+218
-241
lines changed

.env

+3-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ APP_CHAT_RUN_MIGRATIONS=1
1818
############################
1919
# Connect Four Context #
2020
############################
21-
APP_CONNECT_FOUR_DOCTRINE_DBAL_URL=mysqli://root:password@mysql:3306/connect-four?persistent=1
21+
APP_CONNECT_FOUR_DOCTRINE_DBAL_URL=mysqli://root:password@mysql:3306?persistent=1
22+
APP_CONNECT_FOUR_DOCTRINE_DBAL_DATABASE=connect-four
23+
APP_CONNECT_FOUR_DOCTRINE_DBAL_SHARDS=connect-four
2224
APP_CONNECT_FOUR_RUN_MIGRATIONS=1
2325
APP_CONNECT_FOUR_PREDIS_CLIENT_URL=redis://redis:6379?persistent=1
2426

README.md

+12-7
Original file line numberDiff line numberDiff line change
@@ -308,18 +308,23 @@ In the next step we've to scale the databases. We've to divide this into two par
308308
1. First we want to scale the databases for reading purposes.
309309
Since there should not be a concurrency problem in this application, we can add replicas for the MySQL and Redis stores.
310310
2. Then we want to scale the databases for writing purposes.
311-
__Example for connect four__: I've put much effort to allow scaling the Connect Four context properly.
312-
Because we use the CQRS pattern to decouple the queries that span multiple games,
313-
such as counting running games or listing open games, we can ignore this queries in this case.
314-
To fetch the command model we exclusively need a game id.
315-
With this in mind, we can leverage a technique called
311+
__Example for connect four__: This context already can be scaled-out by
316312
[sharding](https://en.wikipedia.org/wiki/Shard_(database_architecture))
317-
for this. The shard key is in our case the game id.
313+
the database, since queries that span multiple games have been offloaded. How this is made possible is described
314+
[in this section](#connect-four).
315+
Only the game id is needed for the execution of the command model,
316+
which is why it's well suited for the sharding key.
317+
Sharding is done at the application level, more specifically in the
318+
[repository](/src/ConnectFour/Port/Adapter/Persistence/Repository/DoctrineJsonGameRepository.php).
319+
The application uses schema-based sharding and is aware of all existing logical shards,
320+
while it's only aware of one physical connection. To actually forward queries to separate physical shards,
321+
a proxy such as ProxySQL can be used. An example will be added with
322+
[#118](https://github.com/marein/php-gaming-website/issues/118).
318323
__Example for chat__: Currently there shouldn't be queries that span multiple chats.
319324
To invoke a chat operation (either writing or reading) we exclusively need a chat id.
320325
As in connect four context, we can use
321326
[sharding](https://en.wikipedia.org/wiki/Shard_(database_architecture))
322-
for the chat context.
327+
for the chat context, where the chat id is the sharding key.
323328

324329
You may have seen that all contexts uses only one MySQL and one Redis instance.
325330
This could be different for the production environment depending on the scale.

bin/connect-four/onEntrypoint

+12-8
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,16 @@ set -e
44

55
if [ "${APP_CONNECT_FOUR_RUN_MIGRATIONS}" = "1" ]
66
then
7-
bin/console doctrine:database:create \
8-
--connection=connect_four \
9-
--if-not-exists
10-
bin/console doctrine:migrations:migrate \
11-
--configuration=config/connect-four/migrations.yml \
12-
--conn=connect_four \
13-
--allow-no-migration \
14-
--no-interaction
7+
IFS=, read -ra values <<< "${APP_CONNECT_FOUR_DOCTRINE_DBAL_SHARDS}"
8+
for value in "${values[@]}"
9+
do
10+
APP_CONNECT_FOUR_DOCTRINE_DBAL_DATABASE=${value} bin/console doctrine:database:create \
11+
--connection=connect_four \
12+
--if-not-exists
13+
APP_CONNECT_FOUR_DOCTRINE_DBAL_DATABASE=${value} bin/console doctrine:migrations:migrate \
14+
--configuration=config/connect-four/migrations.yml \
15+
--conn=connect_four \
16+
--allow-no-migration \
17+
--no-interaction
18+
done
1519
fi

config/connect-four/config.yml

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ doctrine:
66
connections:
77
connect-four:
88
url: '%env(APP_CONNECT_FOUR_DOCTRINE_DBAL_URL)%'
9+
dbname: '%env(APP_CONNECT_FOUR_DOCTRINE_DBAL_DATABASE)%'
910
server_version: 8.0
1011
charset: utf8mb4
1112
default_table_options:

config/connect-four/services/command_bus.yml

+1-8
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,10 @@ services:
1212
class: Gaming\Common\Bus\RetryBus
1313
public: false
1414
arguments:
15-
- '@connect-four.doctrine-transactional-command-bus'
15+
- '@connect-four.routing-command-bus'
1616
- 3
1717
- 'Gaming\Common\Domain\Exception\ConcurrencyException'
1818

19-
connect-four.doctrine-transactional-command-bus:
20-
class: Gaming\Common\Port\Adapter\Bus\DoctrineTransactionalBus
21-
public: false
22-
arguments:
23-
- '@connect-four.routing-command-bus'
24-
- '@connect-four.doctrine-dbal'
25-
2619
# This is pretty ugly. We can use tags, or create this via a factory in php.
2720
connect-four.routing-command-bus:
2821
class: Gaming\Common\Bus\RoutingBus

config/connect-four/services/game.yml

+6-10
Original file line numberDiff line numberDiff line change
@@ -20,24 +20,20 @@ services:
2020
- '@connect-four.doctrine-dbal'
2121
- 'game'
2222
- '@connect-four.domain-event-publisher'
23+
- '@connect-four.event-store'
2324
- '@connect-four.normalizer'
25+
- !service
26+
class: Gaming\Common\Sharding\Integration\Crc32ModShards
27+
arguments:
28+
- '%env(csv:APP_CONNECT_FOUR_DOCTRINE_DBAL_SHARDS)%'
2429

2530
connect-four.game-store:
2631
class: Gaming\ConnectFour\Port\Adapter\Persistence\Repository\PredisGameStore
2732
public: false
2833
arguments:
2934
- '@connect-four.predis'
3035
- 'game.'
31-
- '@connect-four.event-store-game-finder'
32-
33-
connect-four.game-finder:
34-
alias: 'connect-four.game-store'
35-
36-
connect-four.event-store-game-finder:
37-
class: Gaming\ConnectFour\Port\Adapter\Persistence\Repository\EventStoreGameFinder
38-
public: false
39-
arguments:
40-
- '@connect-four.event-store'
36+
- '@connect-four.game-repository'
4137

4238
connect-four.games-by-player-store:
4339
class: Gaming\ConnectFour\Port\Adapter\Persistence\Repository\PredisGamesByPlayerStore

config/connect-four/services/query_bus.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ services:
2222
class: Gaming\ConnectFour\Application\Game\Query\GameHandler
2323
public: false
2424
arguments:
25-
- '@connect-four.game-finder'
25+
- '@connect-four.game-store'
2626

2727
connect-four.query.games-by-player-handler:
2828
class: Gaming\ConnectFour\Application\Game\Query\GamesByPlayerHandler

docker-compose.production.yml

+3-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ x-php-container:
1212
APP_RABBIT_MQ_DSN: "amqp://guest:guest@rabbit-mq:5672?receive_method=basic_consume&qos_prefetch_count=10&heartbeat=60"
1313
APP_CHAT_DOCTRINE_DBAL_URL: "mysqli://root:password@mysql:3306/chat?persistent=1"
1414
APP_CHAT_RUN_MIGRATIONS: "1"
15-
APP_CONNECT_FOUR_DOCTRINE_DBAL_URL: "mysqli://root:password@mysql:3306/connect-four?persistent=1"
15+
APP_CONNECT_FOUR_DOCTRINE_DBAL_URL: "mysqli://root:password@mysql:3306?persistent=1"
16+
APP_CONNECT_FOUR_DOCTRINE_DBAL_DATABASE: "connect-four"
17+
APP_CONNECT_FOUR_DOCTRINE_DBAL_SHARDS: "connect-four"
1618
APP_CONNECT_FOUR_RUN_MIGRATIONS: "1"
1719
APP_CONNECT_FOUR_PREDIS_CLIENT_URL: "redis://redis:6379?persistent=1"
1820
APP_IDENTITY_DOCTRINE_DBAL_URL: "mysqli://root:password@mysql:3306/identity?persistent=1"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Gaming\Common\Sharding\Exception;
6+
7+
use Exception;
8+
9+
final class ShardingException extends Exception
10+
{
11+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Gaming\Common\Sharding\Integration;
6+
7+
use Gaming\Common\Sharding\Exception\ShardingException;
8+
use Gaming\Common\Sharding\Shards;
9+
10+
/**
11+
* This implementation causes problems in the event of re-sharding,
12+
* because it's very likely that values will be reassigned to another shard.
13+
* This can lead to more network calls until everything is back in place.
14+
* It's therefore not suitable for every use case and should be used with caution.
15+
* A better alternative would be to use a consistent hashing algorithm,
16+
* which would reduce the likelihood of values being reassigned to another shard.
17+
*/
18+
final class Crc32ModShards implements Shards
19+
{
20+
/**
21+
* @param string[] $shards
22+
*
23+
* @throws ShardingException
24+
*/
25+
public function __construct(
26+
private readonly array $shards
27+
) {
28+
if (count($this->shards) === 0) {
29+
throw new ShardingException('At least one shard must be specified.');
30+
}
31+
}
32+
33+
public function lookup(string $value): string
34+
{
35+
return $this->shards[crc32($value) % count($this->shards)];
36+
}
37+
}

src/Common/Sharding/Shards.php

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Gaming\Common\Sharding;
6+
7+
use Gaming\Common\Sharding\Exception\ShardingException;
8+
9+
interface Shards
10+
{
11+
/**
12+
* @throws ShardingException
13+
*/
14+
public function lookup(string $value): string;
15+
}

src/ConnectFour/Application/Game/Command/AbortHandler.php

+5-5
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace Gaming\ConnectFour\Application\Game\Command;
66

7+
use Gaming\ConnectFour\Domain\Game\Game;
78
use Gaming\ConnectFour\Domain\Game\GameId;
89
use Gaming\ConnectFour\Domain\Game\Games;
910

@@ -18,10 +19,9 @@ public function __construct(Games $games)
1819

1920
public function __invoke(AbortCommand $command): void
2021
{
21-
$game = $this->games->get(GameId::fromString($command->gameId()));
22-
23-
$game->abort($command->playerId());
24-
25-
$this->games->save($game);
22+
$this->games->update(
23+
GameId::fromString($command->gameId()),
24+
static fn(Game $game) => $game->abort($command->playerId())
25+
);
2626
}
2727
}

src/ConnectFour/Application/Game/Command/AssignChatHandler.php

+5-5
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace Gaming\ConnectFour\Application\Game\Command;
66

7+
use Gaming\ConnectFour\Domain\Game\Game;
78
use Gaming\ConnectFour\Domain\Game\GameId;
89
use Gaming\ConnectFour\Domain\Game\Games;
910

@@ -18,10 +19,9 @@ public function __construct(Games $games)
1819

1920
public function __invoke(AssignChatCommand $command): void
2021
{
21-
$game = $this->games->get(GameId::fromString($command->gameId()));
22-
23-
$game->assignChat($command->chatId());
24-
25-
$this->games->save($game);
22+
$this->games->update(
23+
GameId::fromString($command->gameId()),
24+
static fn(Game $game) => $game->assignChat($command->chatId())
25+
);
2626
}
2727
}

src/ConnectFour/Application/Game/Command/JoinHandler.php

+5-5
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace Gaming\ConnectFour\Application\Game\Command;
66

7+
use Gaming\ConnectFour\Domain\Game\Game;
78
use Gaming\ConnectFour\Domain\Game\GameId;
89
use Gaming\ConnectFour\Domain\Game\Games;
910

@@ -18,10 +19,9 @@ public function __construct(Games $games)
1819

1920
public function __invoke(JoinCommand $command): void
2021
{
21-
$game = $this->games->get(GameId::fromString($command->gameId()));
22-
23-
$game->join($command->playerId());
24-
25-
$this->games->save($game);
22+
$this->games->update(
23+
GameId::fromString($command->gameId()),
24+
static fn(Game $game) => $game->join($command->playerId())
25+
);
2626
}
2727
}

src/ConnectFour/Application/Game/Command/MoveHandler.php

+5-5
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace Gaming\ConnectFour\Application\Game\Command;
66

7+
use Gaming\ConnectFour\Domain\Game\Game;
78
use Gaming\ConnectFour\Domain\Game\GameId;
89
use Gaming\ConnectFour\Domain\Game\Games;
910

@@ -18,10 +19,9 @@ public function __construct(Games $games)
1819

1920
public function __invoke(MoveCommand $command): void
2021
{
21-
$game = $this->games->get(GameId::fromString($command->gameId()));
22-
23-
$game->move($command->playerId(), $command->column());
24-
25-
$this->games->save($game);
22+
$this->games->update(
23+
GameId::fromString($command->gameId()),
24+
static fn(Game $game) => $game->move($command->playerId(), $command->column())
25+
);
2626
}
2727
}

src/ConnectFour/Application/Game/Command/OpenHandler.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ public function __invoke(OpenCommand $command): string
2525
$command->playerId()
2626
);
2727

28-
$this->games->save($game);
28+
$this->games->add($game);
2929

3030
return $game->id()->toString();
3131
}

src/ConnectFour/Application/Game/Command/ResignHandler.php

+5-5
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace Gaming\ConnectFour\Application\Game\Command;
66

7+
use Gaming\ConnectFour\Domain\Game\Game;
78
use Gaming\ConnectFour\Domain\Game\GameId;
89
use Gaming\ConnectFour\Domain\Game\Games;
910

@@ -18,10 +19,9 @@ public function __construct(Games $games)
1819

1920
public function __invoke(ResignCommand $command): void
2021
{
21-
$game = $this->games->get(GameId::fromString($command->gameId()));
22-
23-
$game->resign($command->playerId());
24-
25-
$this->games->save($game);
22+
$this->games->update(
23+
GameId::fromString($command->gameId()),
24+
static fn(Game $game) => $game->resign($command->playerId())
25+
);
2626
}
2727
}

src/ConnectFour/Application/Game/Query/Exception/GameNotFoundException.php

-11
This file was deleted.

src/ConnectFour/Application/Game/Query/GameHandler.php

+3-4
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@
44

55
namespace Gaming\ConnectFour\Application\Game\Query;
66

7-
use Gaming\ConnectFour\Application\Game\Query\Exception\GameNotFoundException;
87
use Gaming\ConnectFour\Application\Game\Query\Model\Game\Game;
98
use Gaming\ConnectFour\Application\Game\Query\Model\Game\GameFinder;
9+
use Gaming\ConnectFour\Domain\Game\Exception\GameNotFoundException;
10+
use Gaming\ConnectFour\Domain\Game\GameId;
1011

1112
final class GameHandler
1213
{
@@ -22,8 +23,6 @@ public function __construct(GameFinder $gameFinder)
2223
*/
2324
public function __invoke(GameQuery $query): Game
2425
{
25-
return $this->gameFinder->find(
26-
$query->gameId()
27-
);
26+
return $this->gameFinder->find(GameId::fromString($query->gameId()));
2827
}
2928
}

src/ConnectFour/Application/Game/Query/Model/Game/GameFinder.php

+3-2
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@
44

55
namespace Gaming\ConnectFour\Application\Game\Query\Model\Game;
66

7-
use Gaming\ConnectFour\Application\Game\Query\Exception\GameNotFoundException;
7+
use Gaming\ConnectFour\Domain\Game\Exception\GameNotFoundException;
8+
use Gaming\ConnectFour\Domain\Game\GameId;
89

910
interface GameFinder
1011
{
1112
/**
1213
* @throws GameNotFoundException
1314
*/
14-
public function find(string $gameId): Game;
15+
public function find(GameId $gameId): Game;
1516
}

0 commit comments

Comments
 (0)