feat: allow passing env vars to workers (#210)

This commit is contained in:
Kévin Dunglas 2023-09-15 12:59:43 +02:00 committed by GitHub
parent d30dbdf96e
commit fb63099a88
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 216 additions and 22 deletions

View File

@ -4,6 +4,7 @@
package caddy package caddy
import ( import (
"errors"
"net/http" "net/http"
"strconv" "strconv"
@ -38,13 +39,19 @@ func (phpInterpreterDestructor) Destruct() error {
} }
type workerConfig struct { type workerConfig struct {
// FileName sets the path to the worker script.
FileName string `json:"file_name,omitempty"` FileName string `json:"file_name,omitempty"`
Num int `json:"num,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 { type FrankenPHPApp struct {
NumThreads int `json:"num_threads,omitempty"` // NumThreads sets the number of PHP threads to start. Default: 2x the number of available CPUs.
Workers []workerConfig `json:"workers,omitempty"` NumThreads int `json:"num_threads,omitempty"`
// Workers configures the worker scripts to start.
Workers []workerConfig `json:"workers,omitempty"`
} }
// CaddyModule returns the Caddy module information. // CaddyModule returns the Caddy module information.
@ -61,7 +68,7 @@ func (f *FrankenPHPApp) Start() error {
opts := []frankenphp.Option{frankenphp.WithNumThreads(f.NumThreads), frankenphp.WithLogger(logger)} opts := []frankenphp.Option{frankenphp.WithNumThreads(f.NumThreads), frankenphp.WithLogger(logger)}
for _, w := range f.Workers { 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) { _, loaded, err := phpInterpreter.LoadOrNew(mainPHPInterpreterKey, func() (caddy.Destructor, error) {
@ -111,11 +118,11 @@ func (f *FrankenPHPApp) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
f.NumThreads = v f.NumThreads = v
case "worker": case "worker":
if !d.NextArg() { wc := workerConfig{}
return d.ArgErr() if d.NextArg() {
wc.FileName = d.Val()
} }
wc := workerConfig{FileName: d.Val()}
if d.NextArg() { if d.NextArg() {
v, err := strconv.Atoi(d.Val()) v, err := strconv.Atoi(d.Val())
if err != nil { if err != nil {
@ -125,6 +132,41 @@ func (f *FrankenPHPApp) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
wc.Num = v 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) f.Workers = append(f.Workers, wc)
} }
} }
@ -147,11 +189,15 @@ func parseGlobalOption(d *caddyfile.Dispenser, _ interface{}) (interface{}, erro
} }
type FrankenPHPModule struct { type FrankenPHPModule struct {
Root string `json:"root,omitempty"` // Root sets the root folder to the site. Default: `root` directive.
SplitPath []string `json:"split_path,omitempty"` Root string `json:"root,omitempty"`
ResolveRootSymlink bool `json:"resolve_root_symlink,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`.
Env map[string]string `json:"env,omitempty"` SplitPath []string `json:"split_path,omitempty"`
logger *zap.Logger // 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
} }
// CaddyModule returns the Caddy module information. // CaddyModule returns the Caddy module information.

View File

@ -73,3 +73,69 @@ func TestLargeRequest(t *testing.T) {
"Request body size: 1048576 (unknown)", "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")
}

View File

@ -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`. 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 ## Environment Variables

View File

@ -563,7 +563,7 @@ sapi_module_struct frankenphp_sapi_module = {
static void *manager_thread(void *arg) { static void *manager_thread(void *arg) {
#ifdef ZTS #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(); php_tsrm_startup();
/*tsrm_error_set(TSRM_ERROR_LEVEL_INFO, NULL);*/ /*tsrm_error_set(TSRM_ERROR_LEVEL_INFO, NULL);*/
# ifdef PHP_WIN32 # ifdef PHP_WIN32

View File

@ -28,6 +28,7 @@ import (
type testOptions struct { type testOptions struct {
workerScript string workerScript string
nbWorkers int nbWorkers int
env map[string]string
nbParrallelRequests int nbParrallelRequests int
realServer bool realServer bool
logger *zap.Logger 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)} initOpts := []frankenphp.Option{frankenphp.WithLogger(opts.logger)}
if opts.workerScript != "" { 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...) initOpts = append(initOpts, opts.initOpts...)

View File

@ -19,6 +19,7 @@ type opt struct {
type workerOpt struct { type workerOpt struct {
fileName string fileName string
num int num int
env map[string]string
} }
// WithNumThreads configures the number of PHP threads to start. // 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. // 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 { return func(o *opt) error {
o.workers = append(o.workers, workerOpt{fileName, num}) o.workers = append(o.workers, workerOpt{fileName, num, env})
return nil return nil
} }

11
testdata/env.php vendored Normal file
View 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'] ?? '';
};

View File

@ -23,7 +23,7 @@ var (
// TODO: start all the worker in parallell to reduce the boot time // TODO: start all the worker in parallell to reduce the boot time
func initWorkers(opt []workerOpt) error { func initWorkers(opt []workerOpt) error {
for _, w := range opt { 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 return err
} }
} }
@ -31,7 +31,7 @@ func initWorkers(opt []workerOpt) error {
return nil 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) absFileName, err := filepath.Abs(fileName)
if err != nil { if err != nil {
return fmt.Errorf("workers %q: %w", fileName, err) return fmt.Errorf("workers %q: %w", fileName, err)
@ -57,8 +57,13 @@ func startWorkers(fileName string, nbWorkers int) error {
for { for {
// Create main dummy request // Create main dummy request
fc := &FrankenPHPContext{ 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( r, err := http.NewRequestWithContext(context.WithValue(
context.Background(), context.Background(),
contextKey, contextKey,
@ -112,7 +117,6 @@ func startWorkers(fileName string, nbWorkers int) error {
return nil 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...)) return fmt.Errorf("workers %q: error while starting: %w", fileName, errors.Join(errs...))
} }

View File

@ -75,10 +75,23 @@ func TestCannotCallHandleRequestInNonWorkerMode(t *testing.T) {
}, nil) }, 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() { func ExampleServeHTTP_workers() {
if err := frankenphp.Init( if err := frankenphp.Init(
frankenphp.WithWorkers("worker1.php", 4), frankenphp.WithWorkers("worker1.php", 4, map[string]string{"ENV1": "foo"}),
frankenphp.WithWorkers("worker2.php", 2), frankenphp.WithWorkers("worker2.php", 2, map[string]string{"ENV2": "bar"}),
); err != nil { ); err != nil {
panic(err) panic(err)
} }