mirror of
https://github.com/dunglas/frankenphp
synced 2024-10-16 17:14:59 +00:00
feat: allow passing env vars to workers (#210)
This commit is contained in:
parent
d30dbdf96e
commit
fb63099a88
@ -4,6 +4,7 @@
|
||||
package caddy
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
@ -38,12 +39,18 @@ func (phpInterpreterDestructor) Destruct() error {
|
||||
}
|
||||
|
||||
type workerConfig struct {
|
||||
// FileName sets the path to the worker script.
|
||||
FileName string `json:"file_name,omitempty"`
|
||||
// Num sets the number of workers to start.
|
||||
Num int `json:"num,omitempty"`
|
||||
// Env sets an extra environment variable to the given value. Can be specified more than once for multiple environment variables.
|
||||
Env map[string]string `json:"env,omitempty"`
|
||||
}
|
||||
|
||||
type FrankenPHPApp struct {
|
||||
// NumThreads sets the number of PHP threads to start. Default: 2x the number of available CPUs.
|
||||
NumThreads int `json:"num_threads,omitempty"`
|
||||
// Workers configures the worker scripts to start.
|
||||
Workers []workerConfig `json:"workers,omitempty"`
|
||||
}
|
||||
|
||||
@ -61,7 +68,7 @@ func (f *FrankenPHPApp) Start() error {
|
||||
|
||||
opts := []frankenphp.Option{frankenphp.WithNumThreads(f.NumThreads), frankenphp.WithLogger(logger)}
|
||||
for _, w := range f.Workers {
|
||||
opts = append(opts, frankenphp.WithWorkers(repl.ReplaceKnown(w.FileName, ""), w.Num))
|
||||
opts = append(opts, frankenphp.WithWorkers(repl.ReplaceKnown(w.FileName, ""), w.Num, w.Env))
|
||||
}
|
||||
|
||||
_, loaded, err := phpInterpreter.LoadOrNew(mainPHPInterpreterKey, func() (caddy.Destructor, error) {
|
||||
@ -111,11 +118,11 @@ func (f *FrankenPHPApp) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||
f.NumThreads = v
|
||||
|
||||
case "worker":
|
||||
if !d.NextArg() {
|
||||
return d.ArgErr()
|
||||
wc := workerConfig{}
|
||||
if d.NextArg() {
|
||||
wc.FileName = d.Val()
|
||||
}
|
||||
|
||||
wc := workerConfig{FileName: d.Val()}
|
||||
if d.NextArg() {
|
||||
v, err := strconv.Atoi(d.Val())
|
||||
if err != nil {
|
||||
@ -125,6 +132,41 @@ func (f *FrankenPHPApp) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||
wc.Num = v
|
||||
}
|
||||
|
||||
for d.NextBlock(1) {
|
||||
v := d.Val()
|
||||
switch v {
|
||||
case "file":
|
||||
if !d.NextArg() {
|
||||
return d.ArgErr()
|
||||
}
|
||||
wc.FileName = d.Val()
|
||||
case "num":
|
||||
if !d.NextArg() {
|
||||
return d.ArgErr()
|
||||
}
|
||||
|
||||
v, err := strconv.Atoi(d.Val())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
wc.Num = v
|
||||
case "env":
|
||||
args := d.RemainingArgs()
|
||||
if len(args) != 2 {
|
||||
return d.ArgErr()
|
||||
}
|
||||
if wc.Env == nil {
|
||||
wc.Env = make(map[string]string)
|
||||
}
|
||||
wc.Env[args[0]] = args[1]
|
||||
}
|
||||
|
||||
if wc.FileName == "" {
|
||||
return errors.New(`The "file" argument must be specified`)
|
||||
}
|
||||
}
|
||||
|
||||
f.Workers = append(f.Workers, wc)
|
||||
}
|
||||
}
|
||||
@ -147,9 +189,13 @@ func parseGlobalOption(d *caddyfile.Dispenser, _ interface{}) (interface{}, erro
|
||||
}
|
||||
|
||||
type FrankenPHPModule struct {
|
||||
// Root sets the root folder to the site. Default: `root` directive.
|
||||
Root string `json:"root,omitempty"`
|
||||
// SplitPath sets the substrings for splitting the URI into two parts. The first matching substring will be used to split the "path info" from the path. The first piece is suffixed with the matching substring and will be assumed as the actual resource (CGI script) name. The second piece will be set to PATH_INFO for the CGI script to use. Default: `.php`.
|
||||
SplitPath []string `json:"split_path,omitempty"`
|
||||
// ResolveRootSymlink enables resolving the `root` directory to its actual value by evaluating a symbolic link, if one exists.
|
||||
ResolveRootSymlink bool `json:"resolve_root_symlink,omitempty"`
|
||||
// Env sets an extra environment variable to the given value. Can be specified more than once for multiple environment variables.
|
||||
Env map[string]string `json:"env,omitempty"`
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
@ -73,3 +73,69 @@ func TestLargeRequest(t *testing.T) {
|
||||
"Request body size: 1048576 (unknown)",
|
||||
)
|
||||
}
|
||||
|
||||
func TestWorker(t *testing.T) {
|
||||
var wg sync.WaitGroup
|
||||
tester := caddytest.NewTester(t)
|
||||
tester.InitServer(`
|
||||
{
|
||||
skip_install_trust
|
||||
admin localhost:2999
|
||||
http_port 9080
|
||||
https_port 9443
|
||||
|
||||
frankenphp {
|
||||
worker ../testdata/index.php 2
|
||||
}
|
||||
}
|
||||
|
||||
localhost:9080 {
|
||||
route {
|
||||
php {
|
||||
root ../testdata
|
||||
}
|
||||
}
|
||||
}
|
||||
`, "caddyfile")
|
||||
|
||||
for i := 0; i < 100; i++ {
|
||||
wg.Add(1)
|
||||
|
||||
go func(i int) {
|
||||
tester.AssertGetResponse(fmt.Sprintf("http://localhost:9080/index.php?i=%d", i), http.StatusOK, fmt.Sprintf("I am by birth a Genevese (%d)", i))
|
||||
wg.Done()
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func TestEnv(t *testing.T) {
|
||||
tester := caddytest.NewTester(t)
|
||||
tester.InitServer(`
|
||||
{
|
||||
skip_install_trust
|
||||
admin localhost:2999
|
||||
http_port 9080
|
||||
https_port 9443
|
||||
|
||||
frankenphp {
|
||||
worker {
|
||||
file ../testdata/env.php
|
||||
num 1
|
||||
env FOO bar
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
localhost:9080 {
|
||||
route {
|
||||
php {
|
||||
root ../testdata
|
||||
env FOO baz
|
||||
}
|
||||
}
|
||||
}
|
||||
`, "caddyfile")
|
||||
|
||||
tester.AssertGetResponse("http://localhost:9080/env.php", http.StatusOK, "bazbar")
|
||||
}
|
||||
|
@ -8,6 +8,58 @@ You can also configure PHP using `php.ini` as usual.
|
||||
|
||||
In the Docker image, the `php.ini` file is located at `/usr/local/lib/php.ini`.
|
||||
|
||||
## Caddy Directives
|
||||
|
||||
To register the FrankenPHP executor, the `frankenphp` directive must be set in Caddy global options, then the `php` HTTP directive must be set under routes serving PHP scripts:
|
||||
|
||||
|
||||
Then, you can use the `php` HTTP directive to execute PHP scripts:
|
||||
|
||||
```caddyfile
|
||||
{
|
||||
frankenphp
|
||||
}
|
||||
|
||||
localhost {
|
||||
route {
|
||||
php {
|
||||
root <directory> # Sets the root folder to the site. Default: `root` directive.
|
||||
split_path <delim...> # Sets the substrings for splitting the URI into two parts. The first matching substring will be used to split the "path info" from the path. The first piece is suffixed with the matching substring and will be assumed as the actual resource (CGI script) name. The second piece will be set to PATH_INFO for the CGI script to use. Default: `.php`
|
||||
resolve_root_symlink # Enables resolving the `root` directory to its actual value by evaluating a symbolic link, if one exists.
|
||||
env <key> <value> # Sets an extra environment variable to the given value. Can be specified more than once for multiple environment variables.
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Optionnaly, the number of threads to create and [worker scripts](worker.md) to start with the server can be specified under the global directive.
|
||||
|
||||
```caddyfile
|
||||
{
|
||||
frankenphp {
|
||||
num_threads <num_threads> # Sets the number of PHP threads to start. Default: 2x the number of available CPUs.
|
||||
worker {
|
||||
file <path> # Sets the path to the worker script.
|
||||
num <num> # Sets the number of PHP threads to start, defaults to 2x the number of available CPUs.
|
||||
env <key> <value> # Sets an extra environment variable to the given value. Can be specified more than once for multiple environment variables.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ...
|
||||
```
|
||||
|
||||
Alternatively, the short form of the `worker` directive can also be used:
|
||||
|
||||
```caddyfile
|
||||
{
|
||||
frankenphp {
|
||||
worker <file> <num>
|
||||
}
|
||||
}
|
||||
|
||||
# ...
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
|
@ -563,7 +563,7 @@ sapi_module_struct frankenphp_sapi_module = {
|
||||
|
||||
static void *manager_thread(void *arg) {
|
||||
#ifdef ZTS
|
||||
// TODO: use tsrm_startup() directly as we now the number of expected threads
|
||||
// TODO: use tsrm_startup() directly as we know the number of expected threads
|
||||
php_tsrm_startup();
|
||||
/*tsrm_error_set(TSRM_ERROR_LEVEL_INFO, NULL);*/
|
||||
# ifdef PHP_WIN32
|
||||
|
@ -28,6 +28,7 @@ import (
|
||||
type testOptions struct {
|
||||
workerScript string
|
||||
nbWorkers int
|
||||
env map[string]string
|
||||
nbParrallelRequests int
|
||||
realServer bool
|
||||
logger *zap.Logger
|
||||
@ -51,7 +52,7 @@ func runTest(t *testing.T, test func(func(http.ResponseWriter, *http.Request), *
|
||||
|
||||
initOpts := []frankenphp.Option{frankenphp.WithLogger(opts.logger)}
|
||||
if opts.workerScript != "" {
|
||||
initOpts = append(initOpts, frankenphp.WithWorkers(testDataDir+opts.workerScript, opts.nbWorkers))
|
||||
initOpts = append(initOpts, frankenphp.WithWorkers(testDataDir+opts.workerScript, opts.nbWorkers, opts.env))
|
||||
}
|
||||
initOpts = append(initOpts, opts.initOpts...)
|
||||
|
||||
|
@ -19,6 +19,7 @@ type opt struct {
|
||||
type workerOpt struct {
|
||||
fileName string
|
||||
num int
|
||||
env map[string]string
|
||||
}
|
||||
|
||||
// WithNumThreads configures the number of PHP threads to start.
|
||||
@ -31,9 +32,9 @@ func WithNumThreads(numThreads int) Option {
|
||||
}
|
||||
|
||||
// WithWorkers configures the PHP workers to start.
|
||||
func WithWorkers(fileName string, num int) Option {
|
||||
func WithWorkers(fileName string, num int, env map[string]string) Option {
|
||||
return func(o *opt) error {
|
||||
o.workers = append(o.workers, workerOpt{fileName, num})
|
||||
o.workers = append(o.workers, workerOpt{fileName, num, env})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
11
testdata/env.php
vendored
Normal file
11
testdata/env.php
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
$workerServer = $_SERVER;
|
||||
|
||||
require_once __DIR__.'/_executor.php';
|
||||
|
||||
return function () use ($workerServer) {
|
||||
echo $_SERVER['FOO'] ?? '';
|
||||
echo $workerServer['FOO'] ?? '';
|
||||
echo $_GET['i'] ?? '';
|
||||
};
|
12
worker.go
12
worker.go
@ -23,7 +23,7 @@ var (
|
||||
// TODO: start all the worker in parallell to reduce the boot time
|
||||
func initWorkers(opt []workerOpt) error {
|
||||
for _, w := range opt {
|
||||
if err := startWorkers(w.fileName, w.num); err != nil {
|
||||
if err := startWorkers(w.fileName, w.num, w.env); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@ -31,7 +31,7 @@ func initWorkers(opt []workerOpt) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func startWorkers(fileName string, nbWorkers int) error {
|
||||
func startWorkers(fileName string, nbWorkers int, env map[string]string) error {
|
||||
absFileName, err := filepath.Abs(fileName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("workers %q: %w", fileName, err)
|
||||
@ -57,8 +57,13 @@ func startWorkers(fileName string, nbWorkers int) error {
|
||||
for {
|
||||
// Create main dummy request
|
||||
fc := &FrankenPHPContext{
|
||||
Env: map[string]string{"SCRIPT_FILENAME": absFileName},
|
||||
Env: make(map[string]string, len(env)+1),
|
||||
}
|
||||
fc.Env["SCRIPT_FILENAME"] = absFileName
|
||||
for k, v := range env {
|
||||
fc.Env[k] = v
|
||||
}
|
||||
|
||||
r, err := http.NewRequestWithContext(context.WithValue(
|
||||
context.Background(),
|
||||
contextKey,
|
||||
@ -112,7 +117,6 @@ func startWorkers(fileName string, nbWorkers int) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Wrapping multiple errors will be available in Go 1.20: https://github.com/golang/go/issues/53435
|
||||
return fmt.Errorf("workers %q: error while starting: %w", fileName, errors.Join(errs...))
|
||||
}
|
||||
|
||||
|
@ -75,10 +75,23 @@ func TestCannotCallHandleRequestInNonWorkerMode(t *testing.T) {
|
||||
}, nil)
|
||||
}
|
||||
|
||||
func TestWorkerEnv(t *testing.T) {
|
||||
runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
|
||||
req := httptest.NewRequest("GET", fmt.Sprintf("http://example.com/env.php?i=%d", i), nil)
|
||||
w := httptest.NewRecorder()
|
||||
handler(w, req)
|
||||
|
||||
resp := w.Result()
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
|
||||
assert.Equal(t, fmt.Sprintf("bar%d", i), string(body))
|
||||
}, &testOptions{workerScript: "env.php", nbWorkers: 1, env: map[string]string{"FOO": "bar"}, nbParrallelRequests: 10})
|
||||
}
|
||||
|
||||
func ExampleServeHTTP_workers() {
|
||||
if err := frankenphp.Init(
|
||||
frankenphp.WithWorkers("worker1.php", 4),
|
||||
frankenphp.WithWorkers("worker2.php", 2),
|
||||
frankenphp.WithWorkers("worker1.php", 4, map[string]string{"ENV1": "foo"}),
|
||||
frankenphp.WithWorkers("worker2.php", 2, map[string]string{"ENV2": "bar"}),
|
||||
); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user