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
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
}

View File

@ -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")
}

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`.
## 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

View File

@ -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

View File

@ -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...)

View File

@ -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
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
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...))
}

View File

@ -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)
}