Skip to content

Commit 37b6175

Browse files
mtawiltaylorotwell
andauthored
Fix Swoole auto worker count in containers (#1101)
* Fix Swoole auto worker count in containers * formatting * formatting --------- Co-authored-by: Taylor Otwell <taylor@laravel.com>
1 parent 48aafd6 commit 37b6175

File tree

2 files changed

+137
-0
lines changed

2 files changed

+137
-0
lines changed

src/Swoole/SwooleExtension.php

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,22 @@
22

33
namespace Laravel\Octane\Swoole;
44

5+
use Closure;
56
use Swoole\Process;
67

78
class SwooleExtension
89
{
10+
/**
11+
* Create a new Swoole extension helper.
12+
*/
13+
public function __construct(
14+
protected ?Closure $isReadable = null,
15+
protected ?Closure $fileGetContents = null,
16+
) {
17+
$this->isReadable ??= static fn (string $path): bool => is_readable($path);
18+
$this->fileGetContents ??= static fn (string $path): string|false => @file_get_contents($path);
19+
}
20+
921
/**
1022
* Determine if the Swoole extension is installed.
1123
*/
@@ -41,6 +53,12 @@ public function setProcessName(string $appName, string $processName): void
4153
*/
4254
public function cpuCount(): int
4355
{
56+
$cgroupCpuCount = $this->containerCpuCount();
57+
58+
if ($cgroupCpuCount !== null) {
59+
return $cgroupCpuCount;
60+
}
61+
4462
if (function_exists('swoole_cpu_num')) {
4563
return swoole_cpu_num();
4664
}
@@ -51,4 +69,45 @@ public function cpuCount(): int
5169

5270
return 1;
5371
}
72+
73+
/**
74+
* Get the CPU count from the container's cgroup CPU quota, if available.
75+
*/
76+
protected function containerCpuCount(): ?int
77+
{
78+
// cgroups v2...
79+
if (($this->isReadable)('/sys/fs/cgroup/cpu.max')) {
80+
$cpuMax = ($this->fileGetContents)('/sys/fs/cgroup/cpu.max');
81+
82+
if ($cpuMax !== false) {
83+
$parts = preg_split('/\s+/', trim($cpuMax));
84+
$quota = $parts[0] ?? null;
85+
$period = isset($parts[1]) ? (int) $parts[1] : 0;
86+
87+
if ($quota !== 'max' && $period > 0) {
88+
return (int) max(1, ceil((int) $quota / $period));
89+
}
90+
}
91+
}
92+
93+
// cgroups v1...
94+
$quotaFile = '/sys/fs/cgroup/cpu/cpu.cfs_quota_us';
95+
$periodFile = '/sys/fs/cgroup/cpu/cpu.cfs_period_us';
96+
97+
if (($this->isReadable)($quotaFile) && ($this->isReadable)($periodFile)) {
98+
$quota = ($this->fileGetContents)($quotaFile);
99+
$period = ($this->fileGetContents)($periodFile);
100+
101+
if ($quota !== false && $period !== false) {
102+
$quota = (int) trim($quota);
103+
$period = (int) trim($period);
104+
105+
if ($quota > 0 && $period > 0) {
106+
return (int) max(1, ceil($quota / $period));
107+
}
108+
}
109+
}
110+
111+
return null;
112+
}
54113
}

tests/SwooleExtensionTest.php

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,82 @@ public function test_cpu_count()
1414

1515
$this->assertTrue($cpuCount > 0);
1616
}
17+
18+
public function test_container_cpu_count_reads_cgroup_v2_quota()
19+
{
20+
$extension = $this->fakeExtensionWithFiles([
21+
'/sys/fs/cgroup/cpu.max' => '200000 100000',
22+
]);
23+
24+
$this->assertSame(2, $extension->readContainerCpuCount());
25+
}
26+
27+
public function test_container_cpu_count_reads_cgroup_v1_quota()
28+
{
29+
$extension = $this->fakeExtensionWithFiles([
30+
'/sys/fs/cgroup/cpu/cpu.cfs_quota_us' => '250000',
31+
'/sys/fs/cgroup/cpu/cpu.cfs_period_us' => '100000',
32+
]);
33+
34+
$this->assertSame(3, $extension->readContainerCpuCount());
35+
}
36+
37+
public function test_container_cpu_count_returns_null_when_unlimited_or_missing()
38+
{
39+
$extension = $this->fakeExtensionWithFiles([
40+
'/sys/fs/cgroup/cpu.max' => 'max 100000',
41+
]);
42+
43+
$this->assertNull($extension->readContainerCpuCount());
44+
}
45+
46+
public function test_container_cpu_count_ignores_invalid_v2_data()
47+
{
48+
$extension = $this->fakeExtensionWithFiles([
49+
'/sys/fs/cgroup/cpu.max' => '200000',
50+
]);
51+
52+
$this->assertNull($extension->readContainerCpuCount());
53+
}
54+
55+
public function test_cpu_count_respects_cgroup_v2_limit()
56+
{
57+
$extension = $this->getMockBuilder(SwooleExtension::class)
58+
->onlyMethods(['containerCpuCount'])
59+
->getMock();
60+
61+
$extension->method('containerCpuCount')->willReturn(2);
62+
63+
$this->assertSame(2, $extension->cpuCount());
64+
}
65+
66+
public function test_cpu_count_falls_back_when_no_cgroup_limit()
67+
{
68+
$extension = $this->getMockBuilder(SwooleExtension::class)
69+
->onlyMethods(['containerCpuCount'])
70+
->getMock();
71+
72+
$extension->method('containerCpuCount')->willReturn(null);
73+
74+
$this->assertTrue($extension->cpuCount() >= 1);
75+
}
76+
77+
protected function fakeExtensionWithFiles(array $files): object
78+
{
79+
return new class($files) extends SwooleExtension
80+
{
81+
public function __construct(private array $files)
82+
{
83+
parent::__construct(
84+
isReadable: fn (string $path): bool => array_key_exists($path, $this->files),
85+
fileGetContents: fn (string $path): string|false => $this->files[$path] ?? false,
86+
);
87+
}
88+
89+
public function readContainerCpuCount(): ?int
90+
{
91+
return $this->containerCpuCount();
92+
}
93+
};
94+
}
1795
}

0 commit comments

Comments
 (0)