From 25fdb98482b01baa87e9db57ad728af882707263 Mon Sep 17 00:00:00 2001 From: Exanlv Date: Thu, 30 Jan 2025 19:40:23 +0100 Subject: [PATCH 1/6] Add orchestrator and basic test --- fakes/LoopFake.php | 89 +++++++++++++++ src/Orchestration/Orchestrator.php | 136 +++++++++++++++++++++++ src/Parts/Gateway.php | 10 ++ src/Parts/GatewayBot.php | 12 ++ src/Parts/SessionStartLimit.php | 13 +++ src/Rest/Gateway.php | 35 ++++++ src/Rest/Rest.php | 2 + tests/Orchestration/OrchestratorTest.php | 83 ++++++++++++++ 8 files changed, 380 insertions(+) create mode 100644 fakes/LoopFake.php create mode 100644 src/Orchestration/Orchestrator.php create mode 100644 src/Parts/Gateway.php create mode 100644 src/Parts/GatewayBot.php create mode 100644 src/Parts/SessionStartLimit.php create mode 100644 src/Rest/Gateway.php create mode 100644 tests/Orchestration/OrchestratorTest.php diff --git a/fakes/LoopFake.php b/fakes/LoopFake.php new file mode 100644 index 00000000..493fc547 --- /dev/null +++ b/fakes/LoopFake.php @@ -0,0 +1,89 @@ +timers[] = ['seconds' => $interval, 'callback' => $callback]; + } + + public function runTimers(?int $seconds = null) + { + $timers = $this->timers; + $this->timers = []; + + if ($seconds === null) { + foreach ($timers as $i => $timer) { + $timer['callback'](); + } + + return; + } + + foreach ($timers as $i => &$timer) { + $timer['seconds'] -= $seconds; + + if ($timer['seconds'] <= 0) { + $timer['callback'](); + unset($timers[$i]); + } + } + + $this->timers = [...$timers, ...$this->timers]; + } + + public function addPeriodicTimer($interval, $callback) + { + } + + public function cancelTimer(TimerInterface $timer) + { + } + + public function futureTick($listener) + { + } + + public function addSignal($signal, $listener) + { + } + + public function removeSignal($signal, $listener) + { + } + + public function run() + { + } + + public function stop() + { + } +} diff --git a/src/Orchestration/Orchestrator.php b/src/Orchestration/Orchestrator.php new file mode 100644 index 00000000..9bce992f --- /dev/null +++ b/src/Orchestration/Orchestrator.php @@ -0,0 +1,136 @@ + + */ + public function init(): PromiseInterface + { + return $this->gateway->getBot()->then(function (GatewayBot $bot) { + $this->totalShards = $this->totalShardsOverride ?? $bot->shards; + + $this->shards = range( + 0, + $this->totalShards - 1 + ); + + return $this->orchestrate($bot->session_start_limit); + }); + } + + /** + * @return PromiseInterface + */ + private function orchestrate(SessionStartLimit $sessionStartLimits): PromiseInterface + { + return $this->startSpawning( + $sessionStartLimits->remaining, + $sessionStartLimits->max_concurrency, + (new DateTime())->setTimestamp($sessionStartLimits->reset_after), + )->then(function () use ($sessionStartLimits) { + $wait = max( + $sessionStartLimits->reset_after - (new DateTime())->getTimestamp(), + 1 + ); + + return new Promise(function (callable $resolve) use ($wait) { + $this->loop->addTimer( + $wait, + fn () => $this->continueOrchestration()->then(fn () => $resolve(null)) + ); + }); + }); + } + + /** + * @return PromiseInterface + */ + private function continueOrchestration(): PromiseInterface + { + return new Promise(function (callable $resolve) { + if (!empty($this->shards)) { + return $this->gateway->getBot()->then( + fn (GatewayBot $bot) => $this->orchestrate($bot->session_start_limit) + ->then(fn () => $resolve(null)) + ); + } + }); + } + + /** + * @return PromiseInterface + */ + private function startSpawning(int $remaining, int $concurrency, DateTime $resetAfter): PromiseInterface + { + return new Promise(function (callable $resolve) use ($remaining, $concurrency, $resetAfter) { + $currentDate = new DateTime(); + + if ($currentDate > $resetAfter) { + $resolve(null); + + return; + } + + if ($remaining === 0) { + $resolve(null); + return; + } + + $shardsToSpawn = $remaining > $concurrency + ? $concurrency + : $remaining; + + $remaining = max($remaining - $concurrency, 0); + + $shards = array_splice($this->shards, 0, $shardsToSpawn); + + $this->allowSpawning($shards); + + $this->loop->addTimer(self::CONCURRENCY_TIMESPAN_SECONDS, function () use ($remaining, $concurrency, $resetAfter, $resolve) { + $this->startSpawning($remaining, $concurrency, $resetAfter)->then(fn () => $resolve()); + }); + }); + } + + /** + * @param int[] $shards + */ + private function allowSpawning(array $shards): void + { + foreach ($shards as $shard) { + $this->emit(self::ALLOW_SPAWN, [$shard, $this->totalShards]); + } + } +} diff --git a/src/Parts/Gateway.php b/src/Parts/Gateway.php new file mode 100644 index 00000000..52f82cd2 --- /dev/null +++ b/src/Parts/Gateway.php @@ -0,0 +1,10 @@ +mapPromise( + $this->http->get( + Endpoint::GATEWAY + ), + PartsGateway::class, + ); + } + + public function getBot(): PromiseInterface + { + return $this->mapPromise( + $this->http->get( + Endpoint::GATEWAY_BOT + ), + PartsGateway::class, + ); + } +} diff --git a/src/Rest/Rest.php b/src/Rest/Rest.php index b45dacb0..1278594d 100644 --- a/src/Rest/Rest.php +++ b/src/Rest/Rest.php @@ -30,6 +30,7 @@ class Rest public readonly GlobalCommand $globalCommand; public readonly Webhook $webhook; public readonly Guild $guild; + public readonly Gateway $gateway; public function __construct(private Http $http, private DataMapper $dataMapper, private LoggerInterface $logger) { @@ -52,5 +53,6 @@ public function __construct(private Http $http, private DataMapper $dataMapper, $this->globalCommand = new GlobalCommand(...$args); $this->webhook = new Webhook(...$args); $this->guild = new Guild(...$args); + $this->gateway = new Gateway(...$args); } } diff --git a/tests/Orchestration/OrchestratorTest.php b/tests/Orchestration/OrchestratorTest.php new file mode 100644 index 00000000..5d48183a --- /dev/null +++ b/tests/Orchestration/OrchestratorTest.php @@ -0,0 +1,83 @@ +url = '::url::'; + $gatewayBot->shards = 15; + $gatewayBot->session_start_limit = new SessionStartLimit(); + $gatewayBot->session_start_limit->total = 18; + $gatewayBot->session_start_limit->remaining = 15; + $gatewayBot->session_start_limit->reset_after = 12321323123123123; + $gatewayBot->session_start_limit->max_concurrency = 5; + + $gateway->shouldReceive() + ->getBot() + ->andReturns(PromiseFake::get($gatewayBot)) + ->once(); + + $spawned = []; + $orchestrator->on(Orchestrator::ALLOW_SPAWN, function (int $shard, int $totalShards) use (&$spawned) { + $spawned[] = [$shard, $totalShards]; + }); + + $orchestrator->init(); + + $this->assertEquals([ + [0, 15], + [1, 15], + [2, 15], + [3, 15], + [4, 15], + ], $spawned); + + $spawned = []; + $loop->runTimers(Orchestrator::CONCURRENCY_TIMESPAN_SECONDS); + + $this->assertEquals([ + [5, 15], + [6, 15], + [7, 15], + [8, 15], + [9, 15], + ], $spawned); + + $spawned = []; + $loop->runTimers(Orchestrator::CONCURRENCY_TIMESPAN_SECONDS); + + $this->assertEquals([ + [10, 15], + [11, 15], + [12, 15], + [13, 15], + [14, 15], + ], $spawned); + + $spawned = []; + $loop->runTimers(null); + + dd($spawned); + } +} From af7c35bceedd486afa685a07bf000603a93a2d12 Mon Sep 17 00:00:00 2001 From: Exanlv Date: Sun, 2 Feb 2025 14:25:54 +0100 Subject: [PATCH 2/6] Use carbon in Orchestrator, make test dynamic --- src/Orchestration/Orchestrator.php | 11 ++- tests/Orchestration/OrchestratorTest.php | 118 ++++++++++++++--------- 2 files changed, 81 insertions(+), 48 deletions(-) diff --git a/src/Orchestration/Orchestrator.php b/src/Orchestration/Orchestrator.php index 9bce992f..6d76bb3e 100644 --- a/src/Orchestration/Orchestrator.php +++ b/src/Orchestration/Orchestrator.php @@ -4,7 +4,8 @@ namespace Ragnarok\Fenrir\Orchestration; -use DateTime; +use Carbon\Carbon; +use DateTimeInterface; use Evenement\EventEmitter; use Ragnarok\Fenrir\Parts\GatewayBot; use Ragnarok\Fenrir\Parts\SessionStartLimit; @@ -58,10 +59,10 @@ private function orchestrate(SessionStartLimit $sessionStartLimits): PromiseInte return $this->startSpawning( $sessionStartLimits->remaining, $sessionStartLimits->max_concurrency, - (new DateTime())->setTimestamp($sessionStartLimits->reset_after), + Carbon::createFromTimestamp($sessionStartLimits->reset_after), )->then(function () use ($sessionStartLimits) { $wait = max( - $sessionStartLimits->reset_after - (new DateTime())->getTimestamp(), + $sessionStartLimits->reset_after - (Carbon::now())->getTimestamp(), 1 ); @@ -92,10 +93,10 @@ private function continueOrchestration(): PromiseInterface /** * @return PromiseInterface */ - private function startSpawning(int $remaining, int $concurrency, DateTime $resetAfter): PromiseInterface + private function startSpawning(int $remaining, int $concurrency, DateTimeInterface $resetAfter): PromiseInterface { return new Promise(function (callable $resolve) use ($remaining, $concurrency, $resetAfter) { - $currentDate = new DateTime(); + $currentDate = Carbon::now(); if ($currentDate > $resetAfter) { $resolve(null); diff --git a/tests/Orchestration/OrchestratorTest.php b/tests/Orchestration/OrchestratorTest.php index 5d48183a..337bbc70 100644 --- a/tests/Orchestration/OrchestratorTest.php +++ b/tests/Orchestration/OrchestratorTest.php @@ -4,6 +4,8 @@ namespace Tests\Ragnarok\Fenrir\Orchestration; +use Carbon\Carbon; +use DateTimeInterface; use Fakes\Ragnarok\Fenrir\LoopFake; use Fakes\Ragnarok\Fenrir\PromiseFake; use Mockery; @@ -12,30 +14,26 @@ use Ragnarok\Fenrir\Orchestration\Orchestrator; use Ragnarok\Fenrir\Parts\GatewayBot; use Ragnarok\Fenrir\Parts\SessionStartLimit; -use Ragnarok\Fenrir\Rest\Gateway; +use Ragnarok\Fenrir\Rest\Gateway as HttpGateway; class OrchestratorTest extends TestCase { - public function testItOrchestrates() + /** @dataProvider orchestrationDataProvider */ + public function testItSpawnsBasedOnGatewayBotInfo(array $gatewayBots, array $expectedSpawns): void { $loop = new LoopFake(); - /** @var Gateway&MockInterface */ - $gateway = Mockery::mock(Gateway::class); + /** @var HttpGateway&MockInterface */ + $gateway = Mockery::mock(HttpGateway::class); $orchestrator = new Orchestrator($loop, $gateway); - $gatewayBot = new GatewayBot(); - $gatewayBot->url = '::url::'; - $gatewayBot->shards = 15; - $gatewayBot->session_start_limit = new SessionStartLimit(); - $gatewayBot->session_start_limit->total = 18; - $gatewayBot->session_start_limit->remaining = 15; - $gatewayBot->session_start_limit->reset_after = 12321323123123123; - $gatewayBot->session_start_limit->max_concurrency = 5; - $gateway->shouldReceive() ->getBot() - ->andReturns(PromiseFake::get($gatewayBot)) + ->andReturnUsing(function () use (&$gatewayBots) { + $this->assertNotEmpty($gatewayBots, 'GatewayBot requested unexpectedly.'); + + return PromiseFake::get(array_shift($gatewayBots)); + }) ->once(); $spawned = []; @@ -45,39 +43,73 @@ public function testItOrchestrates() $orchestrator->init(); - $this->assertEquals([ - [0, 15], - [1, 15], - [2, 15], - [3, 15], - [4, 15], - ], $spawned); + foreach ($expectedSpawns as $expectedSpawn) { + $this->assertEquals($expectedSpawn, $spawned); + $spawned = []; + $loop->runTimers(); + } - $spawned = []; - $loop->runTimers(Orchestrator::CONCURRENCY_TIMESPAN_SECONDS); - - $this->assertEquals([ - [5, 15], - [6, 15], - [7, 15], - [8, 15], - [9, 15], - ], $spawned); + $this->assertEmpty($spawned); + } - $spawned = []; - $loop->runTimers(Orchestrator::CONCURRENCY_TIMESPAN_SECONDS); + private static function getGatewayBot( + int $shards, + int $sessionStartLimitTotal, + int $sessionStartLimitRemaining, + DateTimeInterface $sessionStartLimitResetAfter, + int $sessionStartLimitConcurrency, + string $url = '::url::' + ): GatewayBot { + $gatewayBot = new GatewayBot(); + $gatewayBot->url = $url; + $gatewayBot->shards = $shards; - $this->assertEquals([ - [10, 15], - [11, 15], - [12, 15], - [13, 15], - [14, 15], - ], $spawned); + $gatewayBot->session_start_limit = new SessionStartLimit(); + $gatewayBot->session_start_limit->total = $sessionStartLimitTotal; + $gatewayBot->session_start_limit->remaining = $sessionStartLimitRemaining; + $gatewayBot->session_start_limit->reset_after = $sessionStartLimitResetAfter->getTimestamp(); + $gatewayBot->session_start_limit->max_concurrency = $sessionStartLimitConcurrency; - $spawned = []; - $loop->runTimers(null); + return $gatewayBot; + } - dd($spawned); + public static function orchestrationDataProvider() + { + return [ + 'Ideal conditions, total % concurrency == 0' => [ + 'gatewayBots' => [ + self::getGatewayBot( + 15, + 15, + 15, + Carbon::now()->addMinutes(5), + 5, + ), + ], + 'expectedSpawns' => [ + [ + [0, 15], + [1, 15], + [2, 15], + [3, 15], + [4, 15], + ], + [ + [5, 15], + [6, 15], + [7, 15], + [8, 15], + [9, 15], + ], + [ + [10, 15], + [11, 15], + [12, 15], + [13, 15], + [14, 15], + ], + ], + ], + ]; } } From 6c907b39a9ea2885dc8bd5ad9c9df51a94a77a4a Mon Sep 17 00:00:00 2001 From: Exanlv Date: Sun, 2 Feb 2025 14:51:34 +0100 Subject: [PATCH 3/6] Lay groundwork for asserting it refretches allowance --- tests/Orchestration/OrchestratorTest.php | 158 ++++++++++++++++++++--- 1 file changed, 137 insertions(+), 21 deletions(-) diff --git a/tests/Orchestration/OrchestratorTest.php b/tests/Orchestration/OrchestratorTest.php index 337bbc70..3a7fda59 100644 --- a/tests/Orchestration/OrchestratorTest.php +++ b/tests/Orchestration/OrchestratorTest.php @@ -18,8 +18,11 @@ class OrchestratorTest extends TestCase { + private const EXPECT_SPAWN = 'expect_spawn'; + private const EXPECT_WAIT = 'expect_wait'; + /** @dataProvider orchestrationDataProvider */ - public function testItSpawnsBasedOnGatewayBotInfo(array $gatewayBots, array $expectedSpawns): void + public function testItSpawnsBasedOnGatewayBotInfo(array $gatewayBots, array $expectations): void { $loop = new LoopFake(); /** @var HttpGateway&MockInterface */ @@ -43,10 +46,18 @@ public function testItSpawnsBasedOnGatewayBotInfo(array $gatewayBots, array $exp $orchestrator->init(); - foreach ($expectedSpawns as $expectedSpawn) { - $this->assertEquals($expectedSpawn, $spawned); - $spawned = []; - $loop->runTimers(); + $firstExpectation = array_shift($expectations); + $this->assertEquals($firstExpectation['data'], $spawned); + $spawned = []; + + foreach ($expectations as $expectation) { + switch ($expectation['type']) { + case self::EXPECT_SPAWN: { + $loop->runTimers(); + $this->assertEquals($expectation['data'], $spawned); + $spawned = []; + } + } } $this->assertEmpty($spawned); @@ -86,27 +97,132 @@ public static function orchestrationDataProvider() 5, ), ], - 'expectedSpawns' => [ + 'expectations' => [ + [ + 'type' => self::EXPECT_SPAWN, + 'data' => [ + [0, 15], + [1, 15], + [2, 15], + [3, 15], + [4, 15], + ] + ], + [ + 'type' => self::EXPECT_SPAWN, + 'data' => [ + [5, 15], + [6, 15], + [7, 15], + [8, 15], + [9, 15], + ] + ], + [ + 'type' => self::EXPECT_SPAWN, + 'data' => [ + [10, 15], + [11, 15], + [12, 15], + [13, 15], + [14, 15], + ] + ], + ], + ], + + 'Less ideal conditions, total % concurrency == 3' => [ + 'gatewayBots' => [ + self::getGatewayBot( + 18, + 18, + 18, + Carbon::now()->addMinutes(5), + 5, + ), + ], + 'expectations' => [ + [ + 'type' => self::EXPECT_SPAWN, + 'data' => [ + [0, 18], + [1, 18], + [2, 18], + [3, 18], + [4, 18], + ] + ], + [ + 'type' => self::EXPECT_SPAWN, + 'data' => [ + [5, 18], + [6, 18], + [7, 18], + [8, 18], + [9, 18], + ] + ], + [ + 'type' => self::EXPECT_SPAWN, + 'data' => [ + [10, 18], + [11, 18], + [12, 18], + [13, 18], + [14, 18], + ] + ], + [ + 'type' => self::EXPECT_SPAWN, + 'data' => [ + [15, 18], + [16, 18], + [17, 18], + ] + ], + ], + ], + + 'It spawns only to the limit of remaining before waiting for reset, and fetching new allowance' => [ + 'gatewayBots' => [ + self::getGatewayBot( + 18, + 18, + 15, + Carbon::now()->addMinutes(5), + 5, + ), + ], + 'expectations' => [ [ - [0, 15], - [1, 15], - [2, 15], - [3, 15], - [4, 15], + 'type' => self::EXPECT_SPAWN, + 'data' => [ + [0, 18], + [1, 18], + [2, 18], + [3, 18], + [4, 18], + ] ], [ - [5, 15], - [6, 15], - [7, 15], - [8, 15], - [9, 15], + 'type' => self::EXPECT_SPAWN, + 'data' => [ + [5, 18], + [6, 18], + [7, 18], + [8, 18], + [9, 18], + ] ], [ - [10, 15], - [11, 15], - [12, 15], - [13, 15], - [14, 15], + 'type' => self::EXPECT_SPAWN, + 'data' => [ + [10, 18], + [11, 18], + [12, 18], + [13, 18], + [14, 18], + ] ], ], ], From b66d613fce05a33f587e67ea7a91016078b28a98 Mon Sep 17 00:00:00 2001 From: Exanlv Date: Tue, 4 Feb 2025 08:08:44 +0100 Subject: [PATCH 4/6] Add more comprehensive test --- src/Orchestration/Orchestrator.php | 2 +- tests/Orchestration/OrchestratorTest.php | 105 +++++++++++++++++------ 2 files changed, 81 insertions(+), 26 deletions(-) diff --git a/src/Orchestration/Orchestrator.php b/src/Orchestration/Orchestrator.php index 6d76bb3e..bacc0f5f 100644 --- a/src/Orchestration/Orchestrator.php +++ b/src/Orchestration/Orchestrator.php @@ -120,7 +120,7 @@ private function startSpawning(int $remaining, int $concurrency, DateTimeInterfa $this->allowSpawning($shards); $this->loop->addTimer(self::CONCURRENCY_TIMESPAN_SECONDS, function () use ($remaining, $concurrency, $resetAfter, $resolve) { - $this->startSpawning($remaining, $concurrency, $resetAfter)->then(fn () => $resolve()); + $this->startSpawning($remaining, $concurrency, $resetAfter)->then(fn () => $resolve(null)); }); }); } diff --git a/tests/Orchestration/OrchestratorTest.php b/tests/Orchestration/OrchestratorTest.php index 3a7fda59..2bd0f2a4 100644 --- a/tests/Orchestration/OrchestratorTest.php +++ b/tests/Orchestration/OrchestratorTest.php @@ -19,11 +19,13 @@ class OrchestratorTest extends TestCase { private const EXPECT_SPAWN = 'expect_spawn'; - private const EXPECT_WAIT = 'expect_wait'; + private const EXPECT_RUN_TIMERS = 'expect_run_timers'; /** @dataProvider orchestrationDataProvider */ public function testItSpawnsBasedOnGatewayBotInfo(array $gatewayBots, array $expectations): void { + Carbon::setTestNow(Carbon::createFromTimestamp(1)); + $loop = new LoopFake(); /** @var HttpGateway&MockInterface */ $gateway = Mockery::mock(HttpGateway::class); @@ -46,16 +48,17 @@ public function testItSpawnsBasedOnGatewayBotInfo(array $gatewayBots, array $exp $orchestrator->init(); - $firstExpectation = array_shift($expectations); - $this->assertEquals($firstExpectation['data'], $spawned); - $spawned = []; - foreach ($expectations as $expectation) { switch ($expectation['type']) { case self::EXPECT_SPAWN: { - $loop->runTimers(); $this->assertEquals($expectation['data'], $spawned); $spawned = []; + break; + } + + case self::EXPECT_RUN_TIMERS: { + $loop->runTimers($expectation['data'] ?? null); + break; } } } @@ -107,7 +110,8 @@ public static function orchestrationDataProvider() [3, 15], [4, 15], ] - ], + ], + ['type' => self::EXPECT_RUN_TIMERS], [ 'type' => self::EXPECT_SPAWN, 'data' => [ @@ -118,6 +122,7 @@ public static function orchestrationDataProvider() [9, 15], ] ], + ['type' => self::EXPECT_RUN_TIMERS], [ 'type' => self::EXPECT_SPAWN, 'data' => [ @@ -152,6 +157,7 @@ public static function orchestrationDataProvider() [4, 18], ] ], + ['type' => self::EXPECT_RUN_TIMERS], [ 'type' => self::EXPECT_SPAWN, 'data' => [ @@ -162,6 +168,7 @@ public static function orchestrationDataProvider() [9, 18], ] ], + ['type' => self::EXPECT_RUN_TIMERS], [ 'type' => self::EXPECT_SPAWN, 'data' => [ @@ -172,6 +179,7 @@ public static function orchestrationDataProvider() [14, 18], ] ], + ['type' => self::EXPECT_RUN_TIMERS], [ 'type' => self::EXPECT_SPAWN, 'data' => [ @@ -186,10 +194,18 @@ public static function orchestrationDataProvider() 'It spawns only to the limit of remaining before waiting for reset, and fetching new allowance' => [ 'gatewayBots' => [ self::getGatewayBot( - 18, - 18, + 28, + 28, 15, - Carbon::now()->addMinutes(5), + Carbon::createFromTimestamp(20), + 5, + ), + + self::getGatewayBot( + 28, + 28, + 15, + Carbon::createFromTimestamp(50), 5, ), ], @@ -197,31 +213,70 @@ public static function orchestrationDataProvider() [ 'type' => self::EXPECT_SPAWN, 'data' => [ - [0, 18], - [1, 18], - [2, 18], - [3, 18], - [4, 18], + [0, 28], + [1, 28], + [2, 28], + [3, 28], + [4, 28], ] ], + ['type' => self::EXPECT_RUN_TIMERS], [ 'type' => self::EXPECT_SPAWN, 'data' => [ - [5, 18], - [6, 18], - [7, 18], - [8, 18], - [9, 18], + [5, 28], + [6, 28], + [7, 28], + [8, 28], + [9, 28], ] ], + ['type' => self::EXPECT_RUN_TIMERS], [ 'type' => self::EXPECT_SPAWN, 'data' => [ - [10, 18], - [11, 18], - [12, 18], - [13, 18], - [14, 18], + [10, 28], + [11, 28], + [12, 28], + [13, 28], + [14, 28], + ] + ], + ['type' => self::EXPECT_RUN_TIMERS], + ['type' => self::EXPECT_RUN_TIMERS, 'data' => 15], + [ + 'type' => self::EXPECT_SPAWN, + 'data' => [] + ], + ['type' => self::EXPECT_RUN_TIMERS, 'data' => 10], + [ + 'type' => self::EXPECT_SPAWN, + 'data' => [ + [15, 28], + [16, 28], + [17, 28], + [18, 28], + [19, 28], + ] + ], + ['type' => self::EXPECT_RUN_TIMERS], + [ + 'type' => self::EXPECT_SPAWN, + 'data' => [ + [20, 28], + [21, 28], + [22, 28], + [23, 28], + [24, 28], + ] + ], + ['type' => self::EXPECT_RUN_TIMERS], + [ + 'type' => self::EXPECT_SPAWN, + 'data' => [ + [25, 28], + [26, 28], + [27, 28], ] ], ], From 315b28813d8d17f02d9c6e73347918cd2055204b Mon Sep 17 00:00:00 2001 From: Exanlv Date: Tue, 4 Feb 2025 13:58:04 +0100 Subject: [PATCH 5/6] Fix typing --- src/Rest/Gateway.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Rest/Gateway.php b/src/Rest/Gateway.php index d9f65d2e..b14eb130 100644 --- a/src/Rest/Gateway.php +++ b/src/Rest/Gateway.php @@ -6,6 +6,7 @@ use Discord\Http\Endpoint; use Ragnarok\Fenrir\Parts\Gateway as PartsGateway; +use Ragnarok\Fenrir\Parts\GatewayBot; use React\Promise\PromiseInterface; /** @@ -29,7 +30,7 @@ public function getBot(): PromiseInterface $this->http->get( Endpoint::GATEWAY_BOT ), - PartsGateway::class, + GatewayBot::class, ); } } From be9594ee91f5f25fd0162ed0a1c462ece260db4c Mon Sep 17 00:00:00 2001 From: Exanlv Date: Tue, 28 Oct 2025 11:25:09 +0100 Subject: [PATCH 6/6] Dont assert how many times getBots is called --- tests/Orchestration/OrchestratorTest.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/Orchestration/OrchestratorTest.php b/tests/Orchestration/OrchestratorTest.php index 2bd0f2a4..caf58a67 100644 --- a/tests/Orchestration/OrchestratorTest.php +++ b/tests/Orchestration/OrchestratorTest.php @@ -38,8 +38,7 @@ public function testItSpawnsBasedOnGatewayBotInfo(array $gatewayBots, array $exp $this->assertNotEmpty($gatewayBots, 'GatewayBot requested unexpectedly.'); return PromiseFake::get(array_shift($gatewayBots)); - }) - ->once(); + }); $spawned = []; $orchestrator->on(Orchestrator::ALLOW_SPAWN, function (int $shard, int $totalShards) use (&$spawned) {