Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Breaking: Loopback Request Support #1287

Merged
merged 45 commits into from
Apr 25, 2024
Merged

Breaking: Loopback Request Support #1287

merged 45 commits into from
Apr 25, 2024

Conversation

adamziel
Copy link
Collaborator

@adamziel adamziel commented Apr 20, 2024

A list of breaking changes

Warning

This is breaking change! See the list of public API changes below to assess the impact on your app.

  • BasePHP, WebPHP, and NodePHP no longer accept the serverOptions argument and will not create a PHPRequestHandler instance. Instead, you have to create a new PHPRequestHandler() explicitly.
  • PHPRequestHandler now requires either a processManager option or a phpFactory option.

Description

Adds support for PHP spawning more PHP instances:

const handler = new PHPRequestHandler({
	documentRoot: '/',
	phpFactory: async ({ isPrimary }) => {
		const php = await createPhp(requestHandler);
		if (!isPrimary) {
			proxyFileSystem(
				await requestHandler.getPrimaryPhp(),
				php,
				requestHandler.documentRoot
			);
		}
		return php;
	},
	maxPhpInstances: 2,
});
const php = await handler.getPrimaryPhp();
php.writeFile('/second-script.php', `<?php echo 'Hi from another PHP instance!';`);
php.writeFile(
	'/main.php',
	`<?php 
		require "/wordpress/load.php"; 

		// Can send HTTP requests to self
		echo file_get_contents( get_site_url() . '/second-script.php' );

		// Can execute PHP scripts
		echo exec( 'php /second-script.php ); 
	`
})

await startLocalServer( handler );
await handler.request({ url: '/main.php', });

Without this PR, we only have a single PHP instance running. If that instance needs to run another PHP script while it's running, it will get error 502 as there's no other PHP instance available to handle that.

Closes #1182
Closes #1177

Technical implementation

This PR draws a clear line between BaesPHP and PHPRequestHandler.

Before this PR, BasePHP could be given a serverOptions argument and it would create a new PHPRequestHandler as a convenience. After this PR, BasePHP longer instantiates PHPRequestHandler. Why? Two reasons:

  • We may have multiple PHP instances but only ever want a single PHPRequestHandler to orchestrate them.
  • PHPRequestHandler must know how to start new PHP instances, but BasePHP shouldn't be concerned with that at all.
    separates PHP leverages two major building blocks:

Furthermore, PHPRequestHandler now uses PHPProcessManager to spawn new PHP instances on demand. To create a new request handler, you now need to pass a processManager instance or, alternatively, a phpFactory function.

Ideally, BasePHP would have no more references to PHPRequestHandler after this PR, but unfortunately we cannot fully decouple the two just yet. BasePHP exposes an absoluteUrl, documentRoot, and a request method that are used by Blueprints, and Blueprint steps only get a BasePHP instance without any extra contextual information. This will be easier to resolve once we switch to PHP Blueprints where we'll never call php.request() and we'll also be able to access these contextual information via $_SERVER and such.

Follow up work

  • PHP: Reduce memory requirements – lower the HEAP size from 1GB #1233
  • Create a separate, minimal Emscripten module as a source of the Filesystem. Remove the concept of a "primary" PHP instance.
  • Decouple BasePHP from the PHPRequestHandler. The dependencies are super intertwined now – BasePHP needs a PHPRequestHandler instance, but PHPRequestHandler acts on BasePHP. PHPProcessManager needs both, but also configuring a spawn handler requires a PHPProcessManager.
  • UniversalPHP and such are interchangeable with both BasePHP and WebPHPEndpoint. This isn't intuitive, especially now that WebPHPEndpoint acts on many PHP instances. Let's make BasePHP distinct from WebPHPEndpoint. They will share some methods and props, but will also have disjoint ones.

Pre-requisite PRs

A large chunk of this mega-PR got split into smaller PRs and merged piece by piece in:

Testing instructions

Confirm the unit tests pass.

If you want to actually play with the feature, create the files described in this comment and:

Confirm PHP can run PHP via HTTP by visiting:

Confirm PHP can run PHP via shell by visiting:

Base automatically changed from self-request to trunk April 21, 2024 20:07
adamziel added a commit that referenced this pull request Apr 21, 2024
…() property (#1286)

Removes the public `php.addServerGlobalEntry()` method in favor of a new
`$_SERVER` property for `PHPRequest`. The provided `$_SERVER` entries
only affected the next `run()` call so making them a part of the `run()`
argument is more natural.

## What problem does this solve?

This PR reduces the amount of state stored in the `BasePHP` class,
simplifying [the PHPProcessManager
explorations](#1287).

## Testing instructions

Confirm the unit and e2e tests pass.
@adamziel adamziel force-pushed the experiment-with-clone branch from 094e6b8 to 713e030 Compare April 21, 2024 20:14
adamziel added 13 commits April 21, 2024 22:29
…HP instances equal standing (there's a single authoritative MEMFS that's proxied to all other PHPs)
The request handler needs to decide whether to serve a static asset or
run the PHP interpreter. For static assets it should just reuse the primary
PHP even if there's 50 concurrent requests to serve. However, for
dynamic PHP requests, it needs to grab an available interpreter.
Therefore, it cannot just accept PHP as an argument as serving requests
requires access to ProcessManager.
@adamziel adamziel changed the title Try: PHPPool, cloning BasePHP Loopback request support, PHPProcessManager Apr 22, 2024
adamziel added a commit that referenced this pull request Apr 25, 2024
Adds a PHP Process manager, a class that can spawn maintain a number of
PHP instances at the same time.

The idea is to have:

* A single "primary" PHP instance that's never killed – it contains the
reference filesystem used by all other PHP instances.
* A pool of disposable PHP instances that are spawned to handle a single
request and reaped immediately after.

Spawning new PHP instances is reasonably fast and can happen on demand,
there's no need to keep a pool of "warm" instances that occupies the
memory all the time.

Example usage:

```ts
const mgr = new PHPProcessManager({
	phpFactory: async () => NodePHP.load(RecommendedPHPVersion),
	maxPhpInstances: 4,
});

const instance = await mgr.getInstance();
await instance.php.run({ code });
instance.reap();

```

Related to #1287

## Remaining work

* Add a "context" parameter to `getInstance()` to enable setting the
correct SAPI Name, e.g. `CLI` or `FPM` depending on the purpose the PHP
instance is created for. This would solve issues like #1223.
Alternatively, the consuming program could call `setSapiName()` on the
PHP instance returned by `getInstance()` – but that assumes the factory
did not initialize the web runtime.

## Testing instructions

Confirm the unit tests pass. This PR only adds new code, it does not
plug in the PHPProcessManager class into any request dispatching code
yet.
adamziel added a commit that referenced this pull request Apr 25, 2024
…he remote PHP API client (#1321)

`setSapiName`, `setPhpIniEntry`, and `setPhpIniPath` are methods that
must either be called synchronously before the PHP internal state is
initialized, or with a complex argument that can't be serialized over a
remote connection.

They can only be used in the same process before PHP is fully
initialized and have never worked correctly when connecting a Playground
API client to `remote.html`.

Removing them makes that apparent, prevents confusing API interactions.

In addition, it sets the stage for supporting
a [Loopback
Request](#1287) as
the backend for the API endpoint switches from a single PHP instance to
a PHPRequestHandler, and here we're removing an API surface that needs
to impact all existing and future PHP instances.

## Testing instructions

Confirm the CI checks pass.
@adamziel adamziel changed the title Loopback request support, PHPProcessManager Loopback Request Support Apr 25, 2024
@adamziel
Copy link
Collaborator Author

cc @sejas @wojtekn @kozer – this is coming, wp-now and Studio will probably require adjusting. See the PR description and #1301 for more information – adopting this change will give you extra features and likely simplify your code.

@adamziel
Copy link
Collaborator Author

Alright, let's ship and observe any incoming error reports! I expect a slight increase in out of memory errors on low end phones, which we can solve with #1233, but it should work otherwise. This is exciting 🤞

@adamziel adamziel merged commit 20406ed into trunk Apr 25, 2024
5 checks passed
@adamziel adamziel deleted the experiment-with-clone branch April 25, 2024 10:22
@sejas
Copy link
Collaborator

sejas commented Apr 25, 2024

Thanks for the heads up!

@adamziel adamziel changed the title Loopback Request Support Breaking: Loopback Request Support Apr 25, 2024
@gelubt5
Copy link

gelubt5 commented May 15, 2024

how to modify this to work????

// Use dynamic import for the CommonJS module
const { NodePHP } = await import('@php-wasm/node');

// Load PHP with the specified version and request handler options
const php = await NodePHP.load('8.3', {
requestHandler: {
documentRoot: new URL('./', import.meta.url).pathname,
absoluteUrl: 'http://127.0.0.1'

},

});

php.writeFile('./index.php', <?php echo "Hello " . $_POST['name']; ?>);
await php.run({ scriptPath: './index.php' });

// Or use the familiar HTTP concepts:
const response = await php.request({
method: 'POST',
url: '/index.php',
data: { name: 'John' },
});
console.log(response.text);

throw new Error("No request handler available.");
^

Error: No request handler available.
at _NodePHP.request

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Archived in project
Development

Successfully merging this pull request may close these issues.

Support requests to self (via an instance pool?) POST Requests fail with 502 (Bad Gateway)
3 participants