Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions src/Process/ForkedChildTerminator.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

namespace PHPStan\Process;

use PHPStan\Turbo\ForkedChild;
use PHPStan\Turbo\TurboExtensionEnabler;
use PHPStanTurbo\Runtime;
use function register_shutdown_function;

/**
Expand All @@ -25,9 +25,10 @@
* Everything the parent reads from the child, the results over the socket
* and the captured output, has gone through unbuffered fds by then.
*
* PHP itself has no _exit(); the turbo extension provides it, and fork mode
* from a phar requires the extension anyway (see ForkParallelChecker). A
* source checkout forking without it keeps the plain exit().
* PHP itself has no _exit(); the turbo extension provides it
* (ForkedChild::exitImmediately()), and fork mode from a phar requires the
* extension anyway (see ForkParallelChecker). A source checkout forking
* without it keeps the plain exit().
*/
final class ForkedChildTerminator
{
Expand All @@ -39,7 +40,7 @@ public static function install(): void
}

register_shutdown_function(static function (): void {
Runtime::exitImmediately();
ForkedChild::exitImmediately();
});
}

Expand Down
30 changes: 30 additions & 0 deletions src/Turbo/ForkedChild.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php declare(strict_types = 1);

namespace PHPStan\Turbo;

/**
* What a pcntl_fork()ed child needs from the turbo extension. With the
* extension active (TurboExtensionEnabler::isActive()), the native class
* replaces this one.
*
* @api
*/
#[ShadowedByTurboExtension(implementation: __DIR__ . '/../../turbo-ext/src/ForkedChild.cpp')]
final class ForkedChild
{

/**
* Ends the child with _exit() and the engine's exit status, skipping
* destructors and the module shutdown of every extension the child
* inherited - one waiting for threads the child never had wedges there.
* Register it as the child's last shutdown function.
*
* @return never
* @throws TurboNotActiveException
*/
public static function exitImmediately()
{
throw new TurboNotActiveException();
}

}
20 changes: 20 additions & 0 deletions src/Turbo/TurboNotActiveException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php declare(strict_types = 1);

namespace PHPStan\Turbo;

use Exception;

/**
* Thrown by a method only the active turbo extension implements.
*
* @api
*/
final class TurboNotActiveException extends Exception
{

public function __construct()
{
parent::__construct('The turbo extension is not active.');
}

}
15 changes: 10 additions & 5 deletions turbo-ext/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,11 +110,16 @@ hardcoded in the collector), so a renamed class updates the map on the next
autoloader dump. Shadowed classes are never referenced that way — the native
code holds their class entries itself and instantiates them directly.

Two more `Runtime` entry points serve fork mode (see `ForkParallelChecker`):
`enablePharForkGuard()` gives each pcntl_fork()ed worker a private cursor on
the running phar's fd (`PharForkGuard.cpp`), and `exitImmediately()` —
`_exit()` with the engine's exit status — is how `ForkedChildTerminator` ends
a forked worker. A forked child inherits every loaded extension but none of
Fork mode (see `ForkParallelChecker`) uses two more entry points:
`Runtime::enablePharForkGuard()` gives each pcntl_fork()ed worker a private
cursor on the running phar's fd (`PharForkGuard.cpp`), and
`exitImmediately()` — `_exit()` with the engine's exit status — is how
`ForkedChildTerminator` ends a forked worker. `exitImmediately()` lives on
`PHPStan\Turbo\ForkedChild`, a shadowed class that is part of PHPStan's
`@api` (`ForkedChild.cpp`), because a program that forks its own workers
needs it too; its PHP twin throws `TurboNotActiveException`. The `Runtime`
methods cannot move there: `TurboExtensionEnabler` calls them before
shadowing is active. A forked child inherits every loaded extension but none of
their threads, so PHP's full teardown can wedge in a fork-unsafe extension's
module shutdown (ext-grpc without `grpc.enable_fork_support`); a forked child
that does not exec() must `_exit()` instead.
Expand Down
4 changes: 0 additions & 4 deletions turbo-ext/analysis-stub.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,4 @@ public static function trustTypesUnder(string $prefix): bool
{
}

public static function exitImmediately(): never
{
}

}
63 changes: 63 additions & 0 deletions turbo-ext/src/ForkedChild.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* PHPStanTurbo\ForkedChild — native implementation of
* PHPStan\Turbo\ForkedChild, what a pcntl_fork()ed child needs from the
* extension, as part of PHPStan's API. Unlike PHPStanTurbo\Runtime
* (main.cpp), which TurboExtensionEnabler calls before shadowing is active,
* this class only exists under its PHP name once activateShadowing() ran;
* the PHP twin throws TurboNotActiveException.
*/

#include "support.h"
#include "generated/ForkedChild.h"

namespace sigs = ptdecl::ForkedChild::sig;

#ifdef PHP_WIN32
#include <process.h>
#else
#include <unistd.h>
#endif

static zend_class_entry *pt_ce_forked_child = nullptr;

/* {{{ registration */

#include "reg.h"

PT_MINIT_REGISTRATION(pt_register_forked_child)
{
reg::Class cls("PHPStan\\Turbo\\ForkedChild");
ptdecl::ForkedChild::declareClass(cls);
ptdecl::ForkedChild::declareProperties(cls);

/* exitImmediately() — _exit() for a pcntl_fork()ed worker;
* ForkedChildTerminator registers it as the child's last shutdown
* function.
*
* A forked child inherits the whole parent process, every loaded
* extension with whatever background threads it started included — but
* fork() copies only the calling thread. PHP's exit() then runs
* destructors and each extension's module shutdown, and an extension
* whose shutdown waits for its threads to check out (ext-grpc's
* grpc_shutdown() without grpc.enable_fork_support, for one) waits
* forever for threads the child never had: the worker has delivered its
* results, the parent keeps polling waitpid(), and the run hangs at 100%.
* A forked child that does not exec() must end with _exit() — no
* destructors, no module shutdown, no atexit handlers; that teardown is
* the parent's. The shutdown functions have run by then, so the crash
* report (ForkedChildCrashReporter) is written, and everything the parent
* reads — the results over the socket, the captured output — went
* through unbuffered fds.
*
* The status is the engine's: what exit() was given, 255 after a fatal
* error, 0 otherwise. */
cls.method(sigs::exitImmediately, [](INTERNAL_FUNCTION_PARAMETERS) {
ZEND_PARSE_PARAMETERS_NONE();

_exit(EG(exit_status));
});

cls.shadow(&pt_ce_forked_child);
}

/* }}} */
39 changes: 39 additions & 0 deletions turbo-ext/src/generated/ForkedChild.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/* Generated by turbo-ext/bin/generate-declarations.php from
* src/Turbo/ForkedChild.php — do not edit. */

#ifndef PHPSTANTURBO_GENERATED_FORKED_CHILD_H
#define PHPSTANTURBO_GENERATED_FORKED_CHILD_H

#include "../reg.h"

namespace ptdecl::ForkedChild {

inline void declareClass(reg::Class &cls)
{
cls.final();
}

/* the properties the class declares itself, in declaration order (a used trait's come from its registrar) */
inline void declareProperties(reg::Class &cls)
{
(void) cls;
}

/* the string and parameter tables the signatures below index into (see reg::Sig) */
namespace sigtab {
inline constexpr char strings[] =
"exitImmediately"; /* 0 */
inline constexpr reg::PackedArg args[] = {
reg::packed(0, 0), /* placeholder: C++ has no empty arrays */
};
using Sig = reg::Sig<strings, args>;
} // namespace sigtab

/* the signatures of the methods the class declares itself (a used trait's are in the trait's header) */
namespace sig {
inline constexpr sigtab::Sig exitImmediately = { { 0 /* exitImmediately */, 0, 0, 0, reg::NoArg, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } };
} // namespace sig

} // namespace ptdecl::ForkedChild

#endif
35 changes: 0 additions & 35 deletions turbo-ext/src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,6 @@
#include "Engine.h"
#include "version.h"

#ifdef PHP_WIN32
#include <process.h>
#else
#include <unistd.h>
#endif


/* PHPStanTurbo\Runtime::configure() — cold-path configuration entry point.
* TurboExtensionEnabler passes the generated class map (derived from the
* ReferencedByTurboExtension attributes) that the native code resolves
Expand Down Expand Up @@ -107,33 +100,6 @@ static void ZEND_FASTCALL runtimeTrustTypesUnder(INTERNAL_FUNCTION_PARAMETERS)
RETURN_BOOL(pt_trusted_types_set_prefix(prefix));
}

/* PHPStanTurbo\Runtime::exitImmediately() — _exit() for a pcntl_fork()ed
* worker; ForkedChildTerminator registers it as the child's last shutdown
* function.
*
* A forked child inherits the whole parent process, every loaded extension
* with whatever background threads it started included — but fork() copies
* only the calling thread. PHP's exit() then runs destructors and each
* extension's module shutdown, and an extension whose shutdown waits for
* its threads to check out (ext-grpc's grpc_shutdown() without
* grpc.enable_fork_support, for one) waits forever for threads the child
* never had: the worker has delivered its results, the parent keeps
* polling waitpid(), and the run hangs at 100%. A forked child that does
* not exec() must end with _exit() — no destructors, no module shutdown, no
* atexit handlers; that teardown is the parent's. The shutdown functions
* have run by then, so the crash report (ForkedChildCrashReporter) is
* written, and everything the parent reads — the results over the socket,
* the captured output — went through unbuffered fds.
*
* The status is the engine's: what exit() was given, 255 after a fatal
* error, 0 otherwise. */
static void ZEND_FASTCALL runtimeExitImmediately(INTERNAL_FUNCTION_PARAMETERS)
{
ZEND_PARSE_PARAMETERS_NONE();

_exit(EG(exit_status));
}

/* Whether the shared core loaded next to this library carries the same
* version: a mismatched pair (a torn update of the distributed files) must
* never run together — the extension then registers nothing and reports
Expand Down Expand Up @@ -201,7 +167,6 @@ static PHP_MINIT_FUNCTION(phpstan_turbo)
runtime.method("isShadowing", reg::PublicStatic, 0, {}, runtimeIsShadowing, &returnsBool);
runtime.method("enablePharForkGuard", reg::PublicStatic, 1, { reg::stringArg("pharPath") }, runtimeEnablePharForkGuard);
runtime.method("trustTypesUnder", reg::PublicStatic, 1, { reg::stringArg("prefix") }, runtimeTrustTypesUnder, &returnsBool);
runtime.method("exitImmediately", reg::PublicStatic, 0, {}, runtimeExitImmediately);
runtime.register_();

pt_minit_registrations_run();
Expand Down
45 changes: 30 additions & 15 deletions turbo-ext/tests/exit-immediately.php
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?php declare(strict_types=1);

/**
* Test for Runtime::exitImmediately().
* Test for ForkedChild::exitImmediately().
*
* A pcntl_fork()ed child inherits the parent's whole process, and PHP's
* exit() runs the full teardown there: destructors, then every extension's
Expand All @@ -17,25 +17,27 @@
* Two passes over the same child:
* 1. control, plain exit() - the child must still be alive at the deadline
* (proves the workload hangs)
* 2. exitImmediately() from a shutdown function - the child must be gone
* well within the deadline, with exit()'s status, the earlier shutdown
* function's marker written and the destructor's not
* 2. the native exitImmediately() from a shutdown function - the child must
* be gone well within the deadline, with exit()'s status, the earlier
* shutdown function's marker written and the destructor's not
*
* The PHP twin, which runs without the active extension, must throw
* TurboNotActiveException instead.
*
* Run: php -d extension=$PWD/phpstan_turbo.so tests/exit-immediately.php
*/

const DEADLINE_SECONDS = 3;
const EXIT_STATUS = 7;

if (!extension_loaded('phpstan_turbo')) {
fwrite(STDERR, "phpstan_turbo extension is not loaded\n");
exit(1);
}
if (!function_exists('pcntl_fork')) {
echo "SKIP: pcntl is not available\n";
exit(0);
}

// declares the native class as PHPStanTurbo\ForkedChild next to the PHP twin
require __DIR__ . '/activate-prefixed.php';

final class WedgedTeardown
{

Expand All @@ -57,9 +59,10 @@ public function __destruct()
}

/**
* @param class-string|null $terminator
* @return array{exited: bool, status: int|null, markers: string}
*/
function forkAndExit(bool $terminate): array
function forkAndExit(?string $terminator): array
{
$markers = tmpfile();
if ($markers === false) {
Expand All @@ -78,9 +81,9 @@ function forkAndExit(bool $terminate): array
register_shutdown_function(static function () use ($markers): void {
fwrite($markers, "shutdown\n");
});
if ($terminate) {
register_shutdown_function(static function (): void {
PHPStanTurbo\Runtime::exitImmediately();
if ($terminator !== null) {
register_shutdown_function(static function () use ($terminator): void {
$terminator::exitImmediately();
});
}
exit(EXIT_STATUS);
Expand Down Expand Up @@ -110,11 +113,19 @@ function forkAndExit(bool $terminate): array
return ['exited' => $exited, 'status' => $status, 'markers' => $written];
}

$control = forkAndExit(false);
$control = forkAndExit(null);
printf("control (exit): exited %s, markers %s\n", $control['exited'] ? 'yes' : 'no', json_encode($control['markers']));

$terminated = forkAndExit(true);
printf("exitImmediately(): exited %s, status %s, markers %s\n", $terminated['exited'] ? 'yes' : 'no', var_export($terminated['status'], true), json_encode($terminated['markers']));
$terminated = forkAndExit(\PHPStanTurbo\ForkedChild::class);
printf("native: exited %s, status %s, markers %s\n", $terminated['exited'] ? 'yes' : 'no', var_export($terminated['status'], true), json_encode($terminated['markers']));

$twinThrew = false;
try {
\PHPStan\Turbo\ForkedChild::exitImmediately();
} catch (\PHPStan\Turbo\TurboNotActiveException) {
$twinThrew = true;
}
printf("PHP twin: threw TurboNotActiveException %s\n", $twinThrew ? 'yes' : 'no');

if ($control['exited'] || $control['markers'] !== "shutdown\ndestructor\n") {
fwrite(STDERR, "FAIL: the control child did not wedge in its destructor - the workload no longer models the hang\n");
Expand All @@ -132,5 +143,9 @@ function forkAndExit(bool $terminate): array
fwrite(STDERR, "FAIL: unexpected teardown ran in the child\n");
exit(1);
}
if (!$twinThrew) {
fwrite(STDERR, "FAIL: the PHP twin's exitImmediately() did not throw TurboNotActiveException\n");
exit(1);
}

echo "ALL OK\n";
1 change: 1 addition & 0 deletions turbo-ext/tests/smoke.php
Original file line number Diff line number Diff line change
Expand Up @@ -4659,6 +4659,7 @@ public function getComments(): array
\PHPStan\Cache\ArenaCache::class => 'arena-smoke.php',
\PHPStan\Parser\ParserRunner::class => 'parser-corpus.php',
\PHPStan\Reflection\BetterReflection\SourceLocator\PhpFileCleaner::class => 'php-file-cleaner-corpus.php',
\PHPStan\Turbo\ForkedChild::class => 'exit-immediately.php',
\PHPStan\Reflection\BetterReflection\SourceLocator\SymbolFinderInFiles::class => 'symbol-finder-corpus.php',
\PHPStan\Analyser\ExprHandler\Helper\EarlyTerminatingCallHelper::class => 'walk-trace.php',
\PHPStan\Analyser\ExprHandler\Helper\MethodCallReturnTypeHelper::class => 'walk-trace.php',
Expand Down
Loading