FrankenPHP worker process is killed when using spatie/async pools
What happened?
Description
When running my Laravel application with FrankenPHP in worker mode, the container crashes and restarts every time I use spatie/async to create an asynchronous pool. The issue seems to be that the main FrankenPHP worker process is being killed by an underlying process management mechanism, possibly related to Symfony Process.
The application works flawlessly in every other scenario, with significantly improved performance in worker mode. The crash only occurs specifically when Spatie\Async\Pool is used.
Steps to Reproduce
-
Set up a Laravel 12 application with FrankenPHP in worker mode.
-
Install
spatie/asyncvia Composer. -
Create a simple route or command that uses
Spatie\Async\Poolto execute a task, for example:use Spatie\Async\Pool; Route::get('/async-test', function () { $pool = Pool::create(); $pool->add(function () { // Your async task here sleep(2); return 'Task finished'; })->then(function ($output) { echo $output; }); $pool->wait(); }); -
Access the route.
Expected Behavior
The asynchronous task should be executed by spatie/async without causing the main FrankenPHP worker process to crash. The application should return the response normally.
Actual Behavior
The Spatie\Async\Pool function seems to trigger a process shutdown. The container crashes immediately after the async pool is created, and it is then automatically restarted by Docker.
Environment
- FrankenPHP Version: dunglas/frankenphp:1-php8.3-alpine
- Laravel Version: 12
- PHP Version: 8.3
- Spatie/Async Version: 1.6
- Operating System: macOS (Apple M2 chip)
- Docker: 4.43.2
Possible Cause
The spatie/async package uses pcntl_fork() to create new processes. It is likely that this behavior is incompatible with the long-running worker processes of FrankenPHP. The main worker process might be interpreting the spawned child process as an unexpected termination of a subprocess, leading to its own shutdown to prevent a corrupted state.
This seems to be a common issue with pcntl_fork() and long-running PHP application servers like Swoole and FrankenPHP.
Build Type
Docker (Alpine)
Worker Mode
Yes
Operating System
macOS
CPU Architecture
Apple Silicon
PHP configuration
'./configure' '--prefix=' '--with-valgrind=no' '--disable-shared' '--enable-static' '--disable-all' '--disable-cgi' '--disable-phpdbg' '--with-pic' '--disable-cli' '--disable-fpm' '--enable-embed=static' '--disable-micro' '--with-config-file-path=/etc/frankenphp' '--with-config-file-scan-dir=/etc/frankenphp/php.d' '--disable-opcache-jit' '--enable-zts' '--disable-zend-signals' '--enable-zend-max-execution-timers' '--with-amqp' '--with-librabbitmq-dir=/go/src/app/dist/static-php-cli/buildroot' '--enable-apcu' '--enable-ast' '--enable-bcmath' '--enable-brotli' '--with-bz2=/go/src/app/dist/static-php-cli/buildroot' '--enable-calendar' '--enable-ctype' '--with-curl' '--enable-dba' '--enable-dom' '--enable-exif' '--enable-fileinfo' '--enable-filter' '--enable-ftp' '--with-zlib' '--enable-gd' '--with-freetype' '--with-jpeg' '--with-webp' '--with-avif' '--with-gmp=/go/src/app/dist/static-php-cli/buildroot' '--with-gettext=/go/src/app/dist/static-php-cli/buildroot' '--with-iconv=/go/src/app/dist/static-php-cli/buildroot' '--enable-session' '--enable-igbinary' '--with-imagick=/go/src/app/dist/static-php-cli/buildroot' 'ac_cv_func_omp_pause_resource_all=no' '--enable-intl' '--with-ldap=/go/src/app/dist/static-php-cli/buildroot' '--enable-lz4' '--with-lz4-includedir=/go/src/app/dist/static-php-cli/buildroot' '--enable-mbstring' '--enable-mbregex' '--enable-mysqlnd' '--with-mysqli' '--enable-opcache' '--with-password-argon2' '--enable-parallel' '--enable-pcntl' '--enable-pdo' '--with-pdo-mysql' '--with-pgsql' 'PGSQL_CFLAGS=-I/go/src/app/dist/static-php-cli/buildroot/include' 'PGSQL_LIBS=-L/go/src/app/dist/static-php-cli/buildroot/lib -lpq -lpgcommon -lpgport -lzstd -lldap -llber -lsodium -lgmp -lreadline -lncurses -lssl -lcrypto -lxml2 -lz -licuio -licui18n -licuuc -licudata -lpthread -lm -llzma -liconv -lcharset -ldl -lpthread -lm -lstdc++ -liconv -lcharset -ldl -lpthread -lm -lstdc++ -llzma -liconv -lcharset -ldl -lpthread -lm -lstdc++ -licuio -licui18n -licuuc -licudata -lpthread -lm -ldl -lpthread -lm -lstdc++ -lz -ldl -lpthread -lm -lstdc++ -lxml2 -lz -licuio -licui18n -licuuc -licudata -lpthread -lm -llzma -liconv -lcharset -ldl -lpthread -lm -lstdc++ -lssl -lcrypto -lz -ldl -lpthread -lm -lstdc++ -lncurses -ldl -lpthread -lm -lstdc++ -lreadline -lncurses -ldl -lpthread -lm -lstdc++ -lgmp -ldl -lpthread -lm -lstdc++ -lsodium -ldl -lpthread -lm -lstdc++ -lldap -llber -lsodium -lgmp -lssl -lcrypto -lz -ldl -lpthread -lm -lstdc++ -lzstd -ldl -lpthread -lm -lstdc++' '--with-pdo-pgsql=/go/src/app/dist/static-php-cli/buildroot' '--with-sqlite3=/go/src/app/dist/static-php-cli/buildroot' '--with-pdo-sqlite' '--enable-sqlsrv' '--with-pdo-sqlsrv' '--enable-phar' '--enable-posix' '--enable-protobuf' '--without-libedit' '--with-readline=/go/src/app/dist/static-php-cli/buildroot' 'ac_cv_lib_readline_rl_pending_input=yes' '--enable-redis' '--enable-redis-session' '--enable-redis-igbinary' '--enable-redis-zstd' '--enable-redis-lz4' '--with-liblz4=/go/src/app/dist/static-php-cli/buildroot' '--enable-shmop' '--enable-simplexml' '--enable-soap' '--enable-sockets' '--with-sodium' '--with-ssh2=/go/src/app/dist/static-php-cli/buildroot' '--enable-sysvmsg' '--enable-sysvsem' '--enable-sysvshm' '--with-tidy=/go/src/app/dist/static-php-cli/buildroot' '--enable-tokenizer' '--with-zip=/go/src/app/dist/static-php-cli/buildroot' '--with-xlswriter' '--enable-reader' '--with-openssl=/go/src/app/dist/static-php-cli/buildroot' '--enable-xml' '--enable-xmlreader' '--enable-xmlwriter' '--with-libxml=/go/src/app/dist/static-php-cli/buildroot' '--with-xz' '--with-yaml=/go/src/app/dist/static-php-cli/buildroot' '--enable-zstd' '--with-libzstd=/go/src/app/dist/static-php-cli/buildroot' 'PKG_CONFIG=/go/src/app/dist/static-php-cli/buildroot/bin/pkg-config' 'PKG_CONFIG_PATH=/go/src/app/dist/static-php-cli/buildroot/lib/pkgconfig' 'EXTENSION_DIR=/usr/lib/frankenphp/modules' 'PHP_BUILD_PROVIDER=static-php-cli 2.7.2' 'PHP_BUILD_COMPILER=gcc 4.2.0'
Relevant log output
Maybe this is because Pool uses SIGCHLD to notify the parent when a child process is completed? @AlliBalliBaba, I wonder how frankenphp workers handles this? I'm away from a computer today, otherwise I'd take a gander myself.
I'm not sure using this signal would actually work though. Frankenphp is multi-threaded instead of multi-process (like fpm), so the signal should actually go to the frankenphp process, not the thread. This means it would probably just be an unhandled signal -- as frankenphp wouldn't know how to route the signal to the thread that wants it, and if multiple threads want it, then all bets are off. (another reason why pcntl shouldn't be used with Frankenphp)
I also notice that it just spawns Pool workers using php, which may or may not work well since it would be using the installed php, which may have different extensions installed -- depending on how frankenphp is built.
FrankenPHP is written in Go, and Go doesn't play well with forks. I'm not surprised this doesn't work. The best option would be to use the async extension when available instead of forks. This extension is entirely compatible with FrankenPHP and is shipped by default in the static binary.
@dunglas it doesn't look like it uses forks, it just spawns a new php process and when that process is done, it sends a SIGCHLD signal back to frankenphp.
I performed a docker inspect on the crashed container and found the ExitCode was 139. A quick search reveals that this code typically indicates a segmentation fault, which happens when a process tries to access a restricted area of memory.
This suggests that the calls made by spatie/async are causing a memory conflict within the long-running FrankenPHP worker process. The worker's memory state is likely being corrupted by the spawned child process, leading to a fatal crash.
Context:
I am currently in the process of migrating a company's legacy application to a Docker Swarm environment. My immediate goal is to achieve the highest possible performance with the least amount of code modification. I know that adopting Laravel's native queue system is the correct long-term solution, but for this initial phase, I need to make the application run efficiently with its existing code.
Unfortunately, I cannot provide a minimal reproducible example due to the project's nature. However, I am more than happy to share my docker-compose.yml, Dockerfile, and Caddyfile
Thank you for your attention to this matter.
Does the library spawn new cli processes? If not, it's possible that this might only work with FPM since we're using threads in cgi context.
Will have to try, in any case it seems inefficient to spawn a new process for each operation, there are probably better alternatives to achieve the same.
@jhorlima a segfault can happen for many reasons, and may be completely unrelated to the async package. If I had to take a guess, its that pcntl registers a SIGCHLD handler which gets called from the main frankenphp process while in go, then tries to launch the registered php function from the wrong thread, causing it to crash like this. (callbacks cannot be called from other threads)
It might be worth opening an issue there to return false here:
https://github.com/spatie/async/blob/main/src/Pool.php#L63-L70
when in the context of frankenphp, which will cause it to fallback to a synchronous mode instead of spawning a php process.
Hmm I just tried your reproducer on a simple Laravel 12 spatie/async 1.8 setup and it worked fine, I had to add the line withBinary('php') for it to work:
$pool = Pool::create();
$pool->withBinary('php');
Does adding the line (or updating to 1.8) solve it for you? Otherwise it could also be mac/alpine related, since I'm on linux/bookworm.
Hi everyone,
I've successfully reproduced the bug. I created a basic Laravel 12 project with spatie/async to isolate the issue, and you can find it in this repository: https://github.com/jhorlima/laravel-frankenphp-spatie-async.
The error occurs when I use the Composer Scripts workaround (/usr/local/bin/frankenphp php-cli) as suggested in the documentation. This causes the Docker container to exit with an error code 139. However, when I use the /usr/local/bin/php command directly inside the container, the error does not happen.
I'm now going to remove the workaround from my main project to confirm that the error no longer occurs.
Have you seen this type of issue on other architectures (like x86_64), or does it seem to be to macOS ARM?
Hi, I continued testing on my main project and encountered some issues when using withBinary('php') to add processes to the pool.
I started having problems loading Laravel resources, such as Cache and Models. In some cases, the framework didn't load correctly, forcing me to manually import the Laravel Kernel, and in others, the error 139 kept happening, even without the workaround.
To get around this, I decided to use Octane Concurrency instead of the pool. Although this change required a significant code refactoring, the solution proved to be more robust and elegant, without the need for "hacks" or additional adjustments.
I appreciate everyone's collaboration on this issue.
The issue then probably was somehow related to frankenphp php-cli. If you have php cli installed (like in the Dockerfile, I'd always recommend using that over frankenphp php-cli. frankenphp php-cli is only a fallback if all you have is the static binary. On top of that Laravels 'concurrently' is ofc even better since it also starts the Laravel runtime in the background on the offloaded threads.
There are some PRs in the works to make frankenphp php-cli replicate php-cli better (#1757). In the future we might also have our own kinds of 'task workers' or real concurrency once/if this is merged into php-src.