mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-06 13:10:37 -04:00
fix(reverb): use the uv event loop and raise the open-file limit
The default stream_select() loop fails once a descriptor reaches 1024 (FD_SETSIZE). Install ext-uv and, only when an event loop extension is available, raise the soft open-file limit of Reverb up to 65536. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
1501a44cfe
commit
a0fa21b448
@@ -8,6 +8,8 @@ ARG CLOUDFLARED_VERSION=2025.7.0
|
||||
ARG POSTGRES_VERSION=18
|
||||
# https://nginx.org/en/linux_packages.html
|
||||
ARG NGINX_VERSION=1.31.2-r1
|
||||
# https://pecl.php.net/package/uv
|
||||
ARG PHP_UV_VERSION=0.3.0
|
||||
|
||||
# =================================================================
|
||||
# Get MinIO client
|
||||
@@ -26,6 +28,7 @@ ARG TARGETARCH
|
||||
ARG POSTGRES_VERSION
|
||||
ARG CLOUDFLARED_VERSION
|
||||
ARG NGINX_VERSION
|
||||
ARG PHP_UV_VERSION
|
||||
|
||||
WORKDIR /var/www/html
|
||||
|
||||
@@ -70,7 +73,8 @@ RUN apk add --no-cache \
|
||||
vim
|
||||
|
||||
# Install PHP extensions
|
||||
RUN install-php-extensions sockets
|
||||
# uv: Reverb uses an ext-uv event loop instead of stream_select(), which fails at 1024 open files (FD_SETSIZE)
|
||||
RUN install-php-extensions sockets uv-${PHP_UV_VERSION}
|
||||
|
||||
# Configure shell aliases
|
||||
RUN echo "alias ll='ls -al'" >> /etc/profile && \
|
||||
|
||||
@@ -2,5 +2,21 @@
|
||||
|
||||
cd /var/www/html
|
||||
|
||||
# Every WebSocket connection uses a file descriptor. Raise the soft open-file limit (often 1024)
|
||||
# up to the hard limit, at most 65536. Never lower it, and start Reverb even when this fails.
|
||||
# Only with an event loop extension (uv, ev, event): the default stream_select() loop fails for
|
||||
# every connection once one descriptor is 1024 or higher (FD_SETSIZE), so it keeps the limit.
|
||||
if php -r 'exit(function_exists("uv_loop_new") || class_exists("EvLoop", false) || class_exists("EventBase", false) ? 0 : 1);'; then
|
||||
target=65536
|
||||
hard=$(ulimit -Hn)
|
||||
if [ "$hard" != "unlimited" ] && [ "$hard" -lt "$target" ] 2>/dev/null; then
|
||||
target=$hard
|
||||
fi
|
||||
soft=$(ulimit -Sn)
|
||||
if [ "$soft" != "unlimited" ] && [ "$soft" -lt "$target" ] 2>/dev/null; then
|
||||
ulimit -Sn "$target" 2>/dev/null || echo " WARN Could not raise the open-file limit of Reverb from $soft to $target."
|
||||
fi
|
||||
fi
|
||||
|
||||
echo " INFO Reverb is starting..."
|
||||
exec php artisan reverb:start --host=0.0.0.0 --port=${PUSHER_BACKEND_PORT:-6001}
|
||||
|
||||
@@ -8,6 +8,8 @@ ARG CLOUDFLARED_VERSION=2026.7.3
|
||||
ARG POSTGRES_VERSION=18
|
||||
# https://nginx.org/en/linux_packages.html
|
||||
ARG NGINX_VERSION=1.31.2-r1
|
||||
# https://pecl.php.net/package/uv
|
||||
ARG PHP_UV_VERSION=0.3.0
|
||||
|
||||
# Add user/group
|
||||
ARG USER_ID=9999
|
||||
@@ -74,6 +76,7 @@ ARG TARGETPLATFORM
|
||||
ARG POSTGRES_VERSION
|
||||
ARG CLOUDFLARED_VERSION
|
||||
ARG NGINX_VERSION
|
||||
ARG PHP_UV_VERSION
|
||||
ARG COOLIFY_VERSION
|
||||
ARG CI=true
|
||||
|
||||
@@ -120,6 +123,10 @@ RUN --mount=type=cache,target=/var/cache/apk \
|
||||
lsof \
|
||||
vim
|
||||
|
||||
# Install PHP extensions
|
||||
# uv: Reverb uses an ext-uv event loop instead of stream_select(), which fails at 1024 open files (FD_SETSIZE)
|
||||
RUN install-php-extensions uv-${PHP_UV_VERSION}
|
||||
|
||||
# Configure shell aliases
|
||||
RUN echo "alias ll='ls -al'" >> /etc/profile && \
|
||||
echo "alias a='php artisan'" >> /etc/profile && \
|
||||
|
||||
@@ -2,5 +2,21 @@
|
||||
|
||||
cd /var/www/html
|
||||
|
||||
# Every WebSocket connection uses a file descriptor. Raise the soft open-file limit (often 1024)
|
||||
# up to the hard limit, at most 65536. Never lower it, and start Reverb even when this fails.
|
||||
# Only with an event loop extension (uv, ev, event): the default stream_select() loop fails for
|
||||
# every connection once one descriptor is 1024 or higher (FD_SETSIZE), so it keeps the limit.
|
||||
if php -r 'exit(function_exists("uv_loop_new") || class_exists("EvLoop", false) || class_exists("EventBase", false) ? 0 : 1);'; then
|
||||
target=65536
|
||||
hard=$(ulimit -Hn)
|
||||
if [ "$hard" != "unlimited" ] && [ "$hard" -lt "$target" ] 2>/dev/null; then
|
||||
target=$hard
|
||||
fi
|
||||
soft=$(ulimit -Sn)
|
||||
if [ "$soft" != "unlimited" ] && [ "$soft" -lt "$target" ] 2>/dev/null; then
|
||||
ulimit -Sn "$target" 2>/dev/null || echo " WARN Could not raise the open-file limit of Reverb from $soft to $target."
|
||||
fi
|
||||
fi
|
||||
|
||||
echo " INFO Reverb is starting..."
|
||||
exec php artisan reverb:start --host=0.0.0.0 --port=${PUSHER_BACKEND_PORT:-6001}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\Process;
|
||||
|
||||
/**
|
||||
* Reverb keeps one file descriptor per WebSocket connection. With an event loop extension its s6 run
|
||||
* script raises the soft open-file limit up to the hard limit (at most 65536) and still starts when it
|
||||
* cannot. Without one, ReactPHP uses stream_select(), which fails for every connection once a
|
||||
* descriptor reaches FD_SETSIZE (1024), so the limit must stay.
|
||||
*/
|
||||
beforeEach(function () {
|
||||
$this->bin = sys_get_temp_dir().'/reverb-open-files-'.bin2hex(random_bytes(4));
|
||||
File::makeDirectory($this->bin);
|
||||
// Answers the event loop extension probe (`php -r`) from FAKE_LOOP_EXTENSION, and stands in for
|
||||
// `php artisan reverb:start` by reporting the limit Reverb would get.
|
||||
File::put("{$this->bin}/php", "#!/bin/sh\nif [ \"\$1\" = \"-r\" ]; then [ \"\$FAKE_LOOP_EXTENSION\" = yes ]; exit \$?; fi\necho \"soft=\$(ulimit -Sn)\"\n");
|
||||
chmod("{$this->bin}/php", 0755);
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
File::deleteDirectory($this->bin);
|
||||
});
|
||||
|
||||
function reverbSoftOpenFileLimit(string $runScript, string $hardLimit, string $softLimit, string $bin, bool $loopExtension = true): string
|
||||
{
|
||||
$result = Process::env(['PATH' => "{$bin}:/usr/bin:/bin", 'FAKE_LOOP_EXTENSION' => $loopExtension ? 'yes' : 'no'])->run(sprintf(
|
||||
// The soft limit is lowered first: a hard limit below the current soft limit is invalid.
|
||||
'ulimit -Sn %s && ulimit -Hn %s && exec sh %s',
|
||||
$softLimit,
|
||||
$hardLimit,
|
||||
escapeshellarg(base_path($runScript)),
|
||||
));
|
||||
|
||||
expect($result->successful())->toBeTrue();
|
||||
|
||||
return trim(str($result->output())->afterLast('soft='));
|
||||
}
|
||||
|
||||
it('raises the reverb soft open-file limit up to the hard limit, at most 65536', function (string $runScript, string $hardLimit, string $softLimit, string $expected) {
|
||||
expect(reverbSoftOpenFileLimit($runScript, $hardLimit, $softLimit, $this->bin))->toBe($expected);
|
||||
})->with([
|
||||
'production' => 'docker/production/etc/s6-overlay/s6-rc.d/reverb/run',
|
||||
'development' => 'docker/development/etc/s6-overlay/s6-rc.d/reverb/run',
|
||||
])->with([
|
||||
'hard limit below the cap' => ['4096', '1024', '4096'],
|
||||
'hard limit above the cap' => ['200000', '1024', '65536'],
|
||||
'hard limit equal to the soft limit' => ['1024', '1024', '1024'],
|
||||
'soft limit already above the cap' => ['200000', '100000', '100000'],
|
||||
]);
|
||||
|
||||
it('keeps the reverb soft open-file limit without an event loop extension', function (string $runScript) {
|
||||
expect(reverbSoftOpenFileLimit($runScript, '4096', '1024', $this->bin, loopExtension: false))->toBe('1024');
|
||||
})->with([
|
||||
'production' => 'docker/production/etc/s6-overlay/s6-rc.d/reverb/run',
|
||||
'development' => 'docker/development/etc/s6-overlay/s6-rc.d/reverb/run',
|
||||
]);
|
||||
Reference in New Issue
Block a user