Skip to content

Commit c2db163

Browse files
committed
Update node finding service logic to be single query; add test coverage
1 parent 3decbd1 commit c2db163

File tree

5 files changed

+139
-69
lines changed

5 files changed

+139
-69
lines changed

app/Contracts/Repository/NodeRepositoryInterface.php

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -54,15 +54,4 @@ public function loadNodeAllocations(Node $node, bool $refresh = false): Node;
5454
* @return \Illuminate\Support\Collection
5555
*/
5656
public function getNodesForServerCreation(): Collection;
57-
58-
/**
59-
* Return the IDs of all nodes that exist in the provided locations and have the space
60-
* available to support the additional disk and memory provided.
61-
*
62-
* @param array $locations
63-
* @param int $disk
64-
* @param int $memory
65-
* @return \Illuminate\Support\LazyCollection
66-
*/
67-
public function getNodesWithResourceUse(array $locations, int $disk, int $memory): LazyCollection;
6857
}

app/Repositories/Eloquent/NodeRepository.php

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -171,28 +171,4 @@ public function getNodeWithResourceUsage(int $node_id): Node
171171

172172
return $instance->first();
173173
}
174-
175-
/**
176-
* Return the IDs of all nodes that exist in the provided locations and have the space
177-
* available to support the additional disk and memory provided.
178-
*
179-
* @param array $locations
180-
* @param int $disk
181-
* @param int $memory
182-
* @return \Illuminate\Support\LazyCollection
183-
*/
184-
public function getNodesWithResourceUse(array $locations, int $disk, int $memory): LazyCollection
185-
{
186-
$instance = $this->getBuilder()
187-
->select(['nodes.id', 'nodes.memory', 'nodes.disk', 'nodes.memory_overallocate', 'nodes.disk_overallocate'])
188-
->selectRaw('IFNULL(SUM(servers.memory), 0) as sum_memory, IFNULL(SUM(servers.disk), 0) as sum_disk')
189-
->leftJoin('servers', 'servers.node_id', '=', 'nodes.id')
190-
->where('nodes.public', 1);
191-
192-
if (! empty($locations)) {
193-
$instance->whereIn('nodes.location_id', $locations);
194-
}
195-
196-
return $instance->groupBy('nodes.id')->cursor();
197-
}
198174
}

app/Services/Deployment/FindViableNodesService.php

Lines changed: 23 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,12 @@
33
namespace Pterodactyl\Services\Deployment;
44

55
use Webmozart\Assert\Assert;
6-
use Pterodactyl\Contracts\Repository\NodeRepositoryInterface;
6+
use Pterodactyl\Models\Node;
7+
use Illuminate\Support\LazyCollection;
78
use Pterodactyl\Exceptions\Service\Deployment\NoViableNodeException;
89

910
class FindViableNodesService
1011
{
11-
/**
12-
* @var \Pterodactyl\Contracts\Repository\NodeRepositoryInterface
13-
*/
14-
private $repository;
15-
1612
/**
1713
* @var array
1814
*/
@@ -28,16 +24,6 @@ class FindViableNodesService
2824
*/
2925
protected $memory;
3026

31-
/**
32-
* FindViableNodesService constructor.
33-
*
34-
* @param \Pterodactyl\Contracts\Repository\NodeRepositoryInterface $repository
35-
*/
36-
public function __construct(NodeRepositoryInterface $repository)
37-
{
38-
$this->repository = $repository;
39-
}
40-
4127
/**
4228
* Set the locations that should be searched through to locate available nodes.
4329
*
@@ -46,6 +32,8 @@ public function __construct(NodeRepositoryInterface $repository)
4632
*/
4733
public function setLocations(array $locations): self
4834
{
35+
Assert::allInteger($locations, 'An array of location IDs should be provided when calling setLocations.');
36+
4937
$this->locations = $locations;
5038

5139
return $this;
@@ -90,32 +78,34 @@ public function setMemory(int $memory): self
9078
* are tossed out, as are any nodes marked as non-public, meaning automatic
9179
* deployments should not be done against them.
9280
*
93-
* @return int[]
81+
* @return \Pterodactyl\Models\Node[]|\Illuminate\Support\Collection
9482
* @throws \Pterodactyl\Exceptions\Service\Deployment\NoViableNodeException
9583
*/
96-
public function handle(): array
84+
public function handle()
9785
{
98-
Assert::integer($this->disk, 'Calls to ' . __METHOD__ . ' must have the disk space set as an integer, received %s');
99-
Assert::integer($this->memory, 'Calls to ' . __METHOD__ . ' must have the memory usage set as an integer, received %s');
86+
Assert::integer($this->disk, 'Disk space must be an int, got %s');
87+
Assert::integer($this->memory, 'Memory usage must be an int, got %s');
10088

101-
$nodes = $this->repository->getNodesWithResourceUse($this->locations, $this->disk, $this->memory);
102-
$viable = [];
89+
$query = Node::query()->select('nodes.*')
90+
->selectRaw('IFNULL(SUM(servers.memory), 0) as sum_memory')
91+
->selectRaw('IFNULL(SUM(servers.disk), 0) as sum_disk')
92+
->leftJoin('servers', 'servers.node_id', '=', 'nodes.id')
93+
->where('nodes.public', 1);
10394

104-
foreach ($nodes as $node) {
105-
$memoryLimit = $node->memory * (1 + ($node->memory_overallocate / 100));
106-
$diskLimit = $node->disk * (1 + ($node->disk_overallocate / 100));
107-
108-
if (($node->sum_memory + $this->memory) > $memoryLimit || ($node->sum_disk + $this->disk) > $diskLimit) {
109-
continue;
110-
}
111-
112-
$viable[] = $node->id;
95+
if (! empty($this->locations)) {
96+
$query = $query->whereIn('nodes.location_id', $this->locations);
11397
}
11498

115-
if (empty($viable)) {
99+
$results = $query->groupBy('nodes.id')
100+
->havingRaw('(IFNULL(SUM(servers.memory), 0) + ?) < (nodes.memory * (1 + (nodes.memory_overallocate / 100)))', [ $this->memory ])
101+
->havingRaw('(IFNULL(SUM(servers.disk), 0) + ?) < (nodes.disk * (1 + (nodes.disk_overallocate / 100)))', [ $this->disk ])
102+
->get()
103+
->toBase();
104+
105+
if ($results->isEmpty()) {
116106
throw new NoViableNodeException(trans('exceptions.deployment.no_viable_nodes'));
117107
}
118108

119-
return $viable;
109+
return $results;
120110
}
121111
}

app/Services/Servers/ServerCreationService.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@ private function configureDeployment(array $data, DeploymentObject $deployment):
203203
->handle();
204204

205205
return $this->allocationSelectionService->setDedicated($deployment->isDedicated())
206-
->setNodes($nodes)
206+
->setNodes($nodes->pluck('id')->toArray())
207207
->setPorts($deployment->getPorts())
208208
->handle();
209209
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
<?php
2+
3+
namespace Pterodactyl\Tests\Integration\Services\Deployment;
4+
5+
use Pterodactyl\Models\Node;
6+
use InvalidArgumentException;
7+
use Pterodactyl\Models\Location;
8+
use Illuminate\Support\Collection;
9+
use Pterodactyl\Tests\Integration\IntegrationTestCase;
10+
use Pterodactyl\Services\Deployment\FindViableNodesService;
11+
use Pterodactyl\Exceptions\Service\Deployment\NoViableNodeException;
12+
13+
class FindViableNodesServiceTest extends IntegrationTestCase
14+
{
15+
public function testExceptionIsThrownIfNoDiskSpaceHasBeenSet()
16+
{
17+
$this->expectException(InvalidArgumentException::class);
18+
$this->expectExceptionMessage('Disk space must be an int, got NULL');
19+
20+
$this->getService()->handle();
21+
}
22+
23+
public function testExceptionIsThrownIfNoMemoryHasBeenSet()
24+
{
25+
$this->expectException(InvalidArgumentException::class);
26+
$this->expectExceptionMessage('Memory usage must be an int, got NULL');
27+
28+
$this->getService()->setDisk(10)->handle();
29+
}
30+
31+
public function testExpectedNodeIsReturnedForLocation()
32+
{
33+
/** @var \Pterodactyl\Models\Location[] $locations */
34+
$locations = factory(Location::class)->times(2)->create();
35+
36+
/** @var \Pterodactyl\Models\Node[] $nodes */
37+
$nodes = [
38+
// This node should never be returned.
39+
factory(Node::class)->create([
40+
'location_id' => $locations[0]->id,
41+
'memory' => 10240,
42+
'disk' => 1024 * 100,
43+
]),
44+
factory(Node::class)->create([
45+
'location_id' => $locations[1]->id,
46+
'memory' => 1024,
47+
'disk' => 10240,
48+
'disk_overallocate' => 10,
49+
]),
50+
factory(Node::class)->create([
51+
'location_id' => $locations[1]->id,
52+
'memory' => 1024 * 4,
53+
'memory_overallocate' => 50,
54+
'disk' => 102400,
55+
]),
56+
];
57+
58+
$base = function () use ($locations) {
59+
return $this->getService()->setLocations([ $locations[1]->id ])->setDisk(512);
60+
};
61+
62+
$response = $base()->setMemory(512)->handle();
63+
$this->assertInstanceOf(Collection::class, $response);
64+
$this->assertFalse($response->isEmpty());
65+
$this->assertSame(2, $response->count());
66+
$this->assertSame(2, $response->where('location_id', $locations[1]->id)->count());
67+
68+
$response = $base()->setMemory(2048)->handle();
69+
$this->assertSame(1, $response->count());
70+
$this->assertSame($nodes[2]->id, $response[0]->id);
71+
72+
$response = $base()->setDisk(20480)->setMemory(256)->handle();
73+
$this->assertSame(1, $response->count());
74+
$this->assertSame($nodes[2]->id, $response[0]->id);
75+
76+
$response = $base()->setDisk(11263)->setMemory(256)->handle();
77+
$this->assertSame(2, $response->count());
78+
79+
$servers = Collection::make([
80+
$this->createServerModel(['node_id' => $nodes[1]->id, 'disk' => 5120]),
81+
$this->createServerModel(['node_id' => $nodes[1]->id, 'disk' => 5120]),
82+
]);
83+
84+
$response = $base()->setDisk(1024)->setMemory(256)->handle();
85+
$this->assertSame(1, $response->count());
86+
$this->assertSame($nodes[2]->id, $response[0]->id);
87+
$servers->each->delete();
88+
89+
$this->expectException(NoViableNodeException::class);
90+
$base()->setMemory(10000)->handle();
91+
92+
Collection::make([
93+
$this->createServerModel(['node_id' => $nodes[2]->id, 'memory' => 1024]),
94+
$this->createServerModel(['node_id' => $nodes[2]->id, 'memory' => 1024]),
95+
$this->createServerModel(['node_id' => $nodes[2]->id, 'memory' => 1024]),
96+
$this->createServerModel(['node_id' => $nodes[2]->id, 'memory' => 1024]),
97+
]);
98+
99+
$response = $base()->setMemory(500)->handle();
100+
$this->assertSame(2, $response->count());
101+
$this->assertSame(2, $response->where('location_id', $locations[1]->id)->count());
102+
103+
$response = $base()->setMemory(512)->handle();
104+
$this->assertSame(1, $response->count());
105+
$this->assertSame($nodes[1]->id, $response[0]->id);
106+
}
107+
108+
/**
109+
* @return \Pterodactyl\Services\Deployment\FindViableNodesService
110+
*/
111+
private function getService()
112+
{
113+
return $this->app->make(FindViableNodesService::class);
114+
}
115+
}

0 commit comments

Comments
 (0)