feat: worker matching (#1646)

* Adds 'match' configuration

* test

* Adds Caddy's matcher.

* Adds no-fileserver test.

* Prevents duplicate path calculations and optimizes worker access.

* trigger

* Changes worker->match to match->worker

* Adjusts tests.

* formatting

* Resets implementation to worker->match

* Provisions match path rules.

* Allows matching multiple paths

* Fixes var

* Formatting.

* refactoring.

* Adds 'match' configuration

* test

* Adds Caddy's matcher.

* Adds no-fileserver test.

* Prevents duplicate path calculations and optimizes worker access.

* trigger

* Changes worker->match to match->worker

* Adjusts tests.

* formatting

* Resets implementation to worker->match

* Provisions match path rules.

* Allows matching multiple paths

* Fixes var

* Formatting.

* refactoring.

* Update frankenphp.go

Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>

* Update caddy/workerconfig.go

Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>

* Update caddy/workerconfig.go

Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>

* Update caddy/module.go

Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>

* Update caddy/module.go

Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>

* Fixes suggestion

* Refactoring.

* Adds 'match' configuration

* test

* Adds Caddy's matcher.

* Adds no-fileserver test.

* Prevents duplicate path calculations and optimizes worker access.

* trigger

* Changes worker->match to match->worker

* Adjusts tests.

* formatting

* Resets implementation to worker->match

* Provisions match path rules.

* Allows matching multiple paths

* Fixes var

* Formatting.

* refactoring.

* Adds docs.

* Fixes merge removal.

* Update config.md

* go fmt.

* Adds line ending to static.txt and fixes tests.

* Trigger CI

* fix Markdown CS

---------

Co-authored-by: Alliballibaba <alliballibaba@gmail.com>
Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>
This commit is contained in:
Alexander Stecher
2025-07-01 10:27:11 +02:00
committed by GitHub
parent 94c3fac556
commit fb10b1e8f0
15 changed files with 332 additions and 93 deletions

View File

@@ -1316,3 +1316,112 @@ func TestWorkerRestart(t *testing.T) {
"frankenphp_worker_restarts",
))
}
func TestWorkerMatchDirective(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
skip_install_trust
admin localhost:2999
}
http://localhost:`+testPort+` {
php_server {
root ../testdata/files
worker {
file ../worker-with-counter.php
match /matched-path*
num 1
}
}
}
`, "caddyfile")
// worker is outside of public directory, match anyways
tester.AssertGetResponse("http://localhost:"+testPort+"/matched-path", http.StatusOK, "requests:1")
tester.AssertGetResponse("http://localhost:"+testPort+"/matched-path/anywhere", http.StatusOK, "requests:2")
// 404 on unmatched paths
tester.AssertGetResponse("http://localhost:"+testPort+"/elsewhere", http.StatusNotFound, "")
// static file will be served by the fileserver
expectedFileResponse, err := os.ReadFile("../testdata/files/static.txt")
require.NoError(t, err, "static.txt file must be readable for this test")
tester.AssertGetResponse("http://localhost:"+testPort+"/static.txt", http.StatusOK, string(expectedFileResponse))
}
func TestWorkerMatchDirectiveWithMultipleWorkers(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
skip_install_trust
admin localhost:2999
}
http://localhost:`+testPort+` {
php_server {
root ../testdata
worker {
file worker-with-counter.php
match /counter/*
num 1
}
worker {
file index.php
match /index/*
num 1
}
}
}
`, "caddyfile")
// match 2 workers respectively (in the public directory)
tester.AssertGetResponse("http://localhost:"+testPort+"/counter/sub-path", http.StatusOK, "requests:1")
tester.AssertGetResponse("http://localhost:"+testPort+"/index/sub-path", http.StatusOK, "I am by birth a Genevese (i not set)")
// static file will be served by the fileserver
expectedFileResponse, err := os.ReadFile("../testdata/files/static.txt")
require.NoError(t, err, "static.txt file must be readable for this test")
tester.AssertGetResponse("http://localhost:"+testPort+"/files/static.txt", http.StatusOK, string(expectedFileResponse))
// 404 if the request falls through
tester.AssertGetResponse("http://localhost:"+testPort+"/not-matched", http.StatusNotFound, "")
// serve php file directly as fallback
tester.AssertGetResponse("http://localhost:"+testPort+"/hello.php", http.StatusOK, "Hello from PHP")
// serve worker file directly as fallback
tester.AssertGetResponse("http://localhost:"+testPort+"/index.php", http.StatusOK, "I am by birth a Genevese (i not set)")
}
func TestWorkerMatchDirectiveWithoutFileServer(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
skip_install_trust
admin localhost:2999
}
http://localhost:`+testPort+` {
route {
php_server {
index off
file_server off
root ../testdata/files
worker {
file ../worker-with-counter.php
match /some-path
}
}
respond "Request falls through" 404
}
}
`, "caddyfile")
// find the worker at some-path
tester.AssertGetResponse("http://localhost:"+testPort+"/some-path", http.StatusOK, "requests:1")
// do not find the file at static.txt
// the request should completely fall through the php_server module
tester.AssertGetResponse("http://localhost:"+testPort+"/static.txt", http.StatusNotFound, "Request falls through")
}

View File

@@ -1,4 +1,4 @@
package caddy
package caddy
import (
"testing"

View File

@@ -69,6 +69,21 @@ func (f *FrankenPHPModule) Provision(ctx caddy.Context) error {
return fmt.Errorf(`expected ctx.App("frankenphp") to return *FrankenPHPApp, got nil`)
}
for i, wc := range f.Workers {
// make the file path absolute from the public directory
// this can only be done if the root is definied inside php_server
if !filepath.IsAbs(wc.FileName) && f.Root != "" {
wc.FileName = filepath.Join(f.Root, wc.FileName)
}
// Inherit environment variables from the parent php_server directive
if f.Env != nil {
wc.inheritEnv(f.Env)
}
f.Workers[i] = wc
}
workers, err := fapp.addModuleWorkers(f.Workers...)
if err != nil {
return err
@@ -161,12 +176,11 @@ func (f *FrankenPHPModule) ServeHTTP(w http.ResponseWriter, r *http.Request, _ c
}
}
fullScriptPath, _ := fastabs.FastAbs(documentRoot + "/" + r.URL.Path)
workerName := ""
for _, w := range f.Workers {
if p, _ := fastabs.FastAbs(w.FileName); p == fullScriptPath {
if w.matchesPath(r, documentRoot) {
workerName = w.Name
break
}
}
@@ -230,12 +244,11 @@ func (f *FrankenPHPModule) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
f.ResolveRootSymlink = &v
case "worker":
for d.NextBlock(1) {
wc, err := parseWorkerConfig(d)
if err != nil {
return err
}
for d.NextArg() {
}
// Skip "worker" blocks in the first pass
continue
f.Workers = append(f.Workers, wc)
default:
allowedDirectives := "root, split, env, resolve_root_symlink, worker"
@@ -244,43 +257,13 @@ func (f *FrankenPHPModule) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
}
}
// Second pass: Parse only "worker" blocks
d.Reset()
for d.Next() {
for d.NextBlock(0) {
if d.Val() == "worker" {
wc, err := parseWorkerConfig(d)
if err != nil {
return err
}
// Inherit environment variables from the parent php_server directive
if !filepath.IsAbs(wc.FileName) && f.Root != "" {
wc.FileName = filepath.Join(f.Root, wc.FileName)
}
if f.Env != nil {
if wc.Env == nil {
wc.Env = make(map[string]string)
}
for k, v := range f.Env {
// Only set if not already defined in the worker
if _, exists := wc.Env[k]; !exists {
wc.Env[k] = v
}
}
}
// Check if a worker with this filename already exists in this module
for _, existingWorker := range f.Workers {
if existingWorker.FileName == wc.FileName {
return fmt.Errorf(`workers in a single "php_server" block must not have duplicate filenames: %q`, wc.FileName)
}
}
f.Workers = append(f.Workers, wc)
}
// Check if a worker with this filename already exists in this module
fileNames := make(map[string]struct{}, len(f.Workers))
for _, w := range f.Workers {
if _, ok := fileNames[w.FileName]; ok {
return fmt.Errorf(`workers in a single "php_server" block must not have duplicate filenames: %q`, w.FileName)
}
fileNames[w.FileName] = struct{}{}
}
return nil
@@ -418,6 +401,13 @@ func parsePhpServer(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error)
// unmarshaler can read it from the start
dispenser.Reset()
// the rest of the config is specified by the user
// using the php directive syntax
dispenser.Next() // consume the directive name
if err := phpsrv.UnmarshalCaddyfile(dispenser); err != nil {
return nil, err
}
if frankenphp.EmbeddedAppPath != "" {
if phpsrv.Root == "" {
phpsrv.Root = filepath.Join(frankenphp.EmbeddedAppPath, defaultDocumentRoot)
@@ -433,6 +423,9 @@ func parsePhpServer(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error)
// set up a route list that we'll append to
routes := caddyhttp.RouteList{}
// prepend routes from the 'worker match *' directives
routes = prependWorkerRoutes(routes, h, phpsrv, fsrv, disableFsrv)
// set the list of allowed path segments on which to split
phpsrv.SplitPath = extensions
@@ -521,15 +514,6 @@ func parsePhpServer(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error)
"path": h.JSON(pathList),
}
// the rest of the config is specified by the user
// using the php directive syntax
dispenser.Next() // consume the directive name
err = phpsrv.UnmarshalCaddyfile(dispenser)
if err != nil {
return nil, err
}
// create the PHP route which is
// conditional on matching PHP files
phpRoute := caddyhttp.Route{
@@ -576,3 +560,52 @@ func parsePhpServer(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error)
},
}, nil
}
// workers can also match a path without being in the public directory
// in this case we need to prepend the worker routes to the existing routes
func prependWorkerRoutes(routes caddyhttp.RouteList, h httpcaddyfile.Helper, f FrankenPHPModule, fsrv caddy.Module, disableFsrv bool) caddyhttp.RouteList {
allWorkerMatches := caddyhttp.MatchPath{}
for _, w := range f.Workers {
for _, path := range w.MatchPath {
allWorkerMatches = append(allWorkerMatches, path)
}
}
if len(allWorkerMatches) == 0 {
return routes
}
// if there are match patterns, we need to check for files beforehand
if !disableFsrv {
routes = append(routes, caddyhttp.Route{
MatcherSetsRaw: []caddy.ModuleMap{
caddy.ModuleMap{
"file": h.JSON(fileserver.MatchFile{
TryFiles: []string{"{http.request.uri.path}"},
Root: f.Root,
}),
"not": h.JSON(caddyhttp.MatchNot{
MatcherSetsRaw: []caddy.ModuleMap{
{"path": h.JSON(caddyhttp.MatchPath{"*.php"})},
},
}),
},
},
HandlersRaw: []json.RawMessage{
caddyconfig.JSONModuleObject(fsrv, "handler", "file_server", nil),
},
})
}
// forward matching routes to the PHP handler
routes = append(routes, caddyhttp.Route{
MatcherSetsRaw: []caddy.ModuleMap{
caddy.ModuleMap{"path": h.JSON(allWorkerMatches)},
},
HandlersRaw: []json.RawMessage{
caddyconfig.JSONModuleObject(f, "handler", "php", nil),
},
})
return routes
}

View File

@@ -2,11 +2,15 @@ package caddy
import (
"errors"
"net/http"
"path/filepath"
"strconv"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/dunglas/frankenphp"
"github.com/dunglas/frankenphp/internal/fastabs"
)
// workerConfig represents the "worker" directive in the Caddyfile
@@ -29,6 +33,8 @@ type workerConfig struct {
Env map[string]string `json:"env,omitempty"`
// Directories to watch for file changes
Watch []string `json:"watch,omitempty"`
// The path to match against the worker
MatchPath []string `json:"match_path,omitempty"`
// MaxConsecutiveFailures sets the maximum number of consecutive failures before panicking (defaults to 6, set to -1 to never panick)
MaxConsecutiveFailures int `json:"max_consecutive_failures,omitempty"`
}
@@ -96,6 +102,12 @@ func parseWorkerConfig(d *caddyfile.Dispenser) (workerConfig, error) {
} else {
wc.Watch = append(wc.Watch, d.Val())
}
case "match":
// provision the path so it's identical to Caddy match rules
// see: https://github.com/caddyserver/caddy/blob/master/modules/caddyhttp/matchers.go
caddyMatchPath := (caddyhttp.MatchPath)(d.RemainingArgs())
caddyMatchPath.Provision(caddy.Context{})
wc.MatchPath = ([]string)(caddyMatchPath)
case "max_consecutive_failures":
if !d.NextArg() {
return wc, d.ArgErr()
@@ -111,7 +123,7 @@ func parseWorkerConfig(d *caddyfile.Dispenser) (workerConfig, error) {
wc.MaxConsecutiveFailures = int(v)
default:
allowedDirectives := "name, file, num, env, watch, max_consecutive_failures"
allowedDirectives := "name, file, num, env, watch, match, max_consecutive_failures"
return wc, wrongSubDirectiveError("worker", allowedDirectives, v)
}
}
@@ -126,3 +138,29 @@ func parseWorkerConfig(d *caddyfile.Dispenser) (workerConfig, error) {
return wc, nil
}
func (wc workerConfig) inheritEnv(env map[string]string) {
if wc.Env == nil {
wc.Env = make(map[string]string, len(env))
}
for k, v := range env {
// do not overwrite existing environment variables
if _, exists := wc.Env[k]; !exists {
wc.Env[k] = v
}
}
}
func (wc workerConfig) matchesPath(r *http.Request, documentRoot string) bool {
// try to match against a pattern if one is assigned
if len(wc.MatchPath) != 0 {
return (caddyhttp.MatchPath)(wc.MatchPath).Match(r)
}
// if there is no pattern, try to match against the actual path (in the public directory)
fullScriptPath, _ := fastabs.FastAbs(documentRoot + "/" + r.URL.Path)
absFileName, _ := fastabs.FastAbs(wc.FileName)
return fullScriptPath == absFileName
}

View File

@@ -17,12 +17,12 @@ type frankenPHPContext struct {
logger *slog.Logger
request *http.Request
originalRequest *http.Request
worker *worker
docURI string
pathInfo string
scriptName string
scriptFilename string
workerName string
// Whether the request is already closed by us
isDone bool
@@ -89,8 +89,16 @@ func NewRequestWithContext(r *http.Request, opts ...RequestOption) (*http.Reques
}
}
// SCRIPT_FILENAME is the absolute path of SCRIPT_NAME
fc.scriptFilename = sanitizedPathJoin(fc.documentRoot, fc.scriptName)
// if a worker is assigned explicitly, use its filename
// determine if the filename belongs to a worker otherwise
if fc.worker != nil {
fc.scriptFilename = fc.worker.fileName
} else {
// SCRIPT_FILENAME is the absolute path of SCRIPT_NAME
fc.scriptFilename = sanitizedPathJoin(fc.documentRoot, fc.scriptName)
fc.worker = getWorkerByPath(fc.scriptFilename)
}
c := context.WithValue(r.Context(), contextKey, fc)
return r.WithContext(c), nil

View File

@@ -154,6 +154,7 @@ php_server [<matcher>] {
name <name> # Sets the name for the worker, used in logs and metrics. Default: absolute path of worker file. Always starts with m# when defined in a php_server block.
watch <path> # Sets the path to watch for file changes. Can be specified more than once for multiple paths.
env <key> <value> # Sets an extra environment variable to the given value. Can be specified more than once for multiple environment variables. Environment variables for this worker are also inherited from the php_server parent, but can be overwritten here.
match <path> # match the worker to a path pattern. Overrides try_files and can only be used in the php_server directive.
}
worker <other_file> <num> # Can also use the short form like in the global frankenphp block.
}
@@ -204,6 +205,29 @@ where the FrankenPHP process was started. You can instead also specify one or mo
The file watcher is based on [e-dant/watcher](https://github.com/e-dant/watcher).
## Matching the worker to a path
In traditional PHP applications, scripts are always placed in the public directory.
This is also true for worker scripts, which are treated like any other PHP script.
If you want to instead put the worker script outside the public directory, you can do so via the `match` directive.
The `match` directive is an optimized alternative to `try_files` only available inside `php_server` and `php`.
The following example will always serve a file in the public directory if present
and otherwise forward the request to the worker matching the path pattern.
```caddyfile
{
frankenphp {
php_server {
worker {
file /path/to/worker.php # file can be outside of public path
match /api/* # all requests starting with /api/ will be handled by this worker
}
}
}
}
```
### Full Duplex (HTTP/1)
When using HTTP/1.x, it may be desirable to enable full-duplex mode to allow writing a response before the entire body

View File

@@ -403,8 +403,9 @@ func ServeHTTP(responseWriter http.ResponseWriter, request *http.Request) error
}
// Detect if a worker is available to handle this request
if worker, ok := workers[getWorkerKey(fc.workerName, fc.scriptFilename)]; ok {
worker.handleRequest(fc)
if fc.worker != nil {
fc.worker.handleRequest(fc)
return nil
}

View File

@@ -189,8 +189,9 @@ func TestFinishBootingAWorkerScript(t *testing.T) {
}
func TestReturnAnErrorIf2WorkersHaveTheSameFileName(t *testing.T) {
workers = make(map[string]*worker)
_, err1 := newWorker(workerOpt{fileName: "filename.php", maxConsecutiveFailures: defaultMaxConsecutiveFailures})
workers = []*worker{}
w, err1 := newWorker(workerOpt{fileName: "filename.php", maxConsecutiveFailures: defaultMaxConsecutiveFailures})
workers = append(workers, w)
_, err2 := newWorker(workerOpt{fileName: "filename.php", maxConsecutiveFailures: defaultMaxConsecutiveFailures})
assert.NoError(t, err1)
@@ -198,8 +199,9 @@ func TestReturnAnErrorIf2WorkersHaveTheSameFileName(t *testing.T) {
}
func TestReturnAnErrorIf2ModuleWorkersHaveTheSameName(t *testing.T) {
workers = make(map[string]*worker)
_, err1 := newWorker(workerOpt{fileName: "filename.php", name: "workername", maxConsecutiveFailures: defaultMaxConsecutiveFailures})
workers = []*worker{}
w, err1 := newWorker(workerOpt{fileName: "filename.php", name: "workername", maxConsecutiveFailures: defaultMaxConsecutiveFailures})
workers = append(workers, w)
_, err2 := newWorker(workerOpt{fileName: "filename2.php", name: "workername", maxConsecutiveFailures: defaultMaxConsecutiveFailures})
assert.NoError(t, err1)
@@ -208,13 +210,14 @@ func TestReturnAnErrorIf2ModuleWorkersHaveTheSameName(t *testing.T) {
func getDummyWorker(fileName string) *worker {
if workers == nil {
workers = make(map[string]*worker)
workers = []*worker{}
}
worker, _ := newWorker(workerOpt{
fileName: testDataPath + "/" + fileName,
num: 1,
maxConsecutiveFailures: defaultMaxConsecutiveFailures,
})
workers = append(workers, worker)
return worker
}
@@ -241,9 +244,9 @@ func allPossibleTransitions(worker1Path string, worker2Path string) []func(*phpT
thread.boot()
}
},
func(thread *phpThread) { convertToWorkerThread(thread, workers[worker1Path]) },
func(thread *phpThread) { convertToWorkerThread(thread, getWorkerByPath(worker1Path)) },
convertToInactiveThread,
func(thread *phpThread) { convertToWorkerThread(thread, workers[worker2Path]) },
func(thread *phpThread) { convertToWorkerThread(thread, getWorkerByPath(worker2Path)) },
convertToInactiveThread,
}
}

View File

@@ -127,7 +127,9 @@ func WithRequestLogger(logger *slog.Logger) RequestOption {
// WithWorkerName sets the worker that should handle the request
func WithWorkerName(name string) RequestOption {
return func(o *frankenPHPContext) error {
o.workerName = name
if name != "" {
o.worker = getWorkerByName(name)
}
return nil
}

View File

@@ -158,8 +158,8 @@ func startUpscalingThreads(maxScaledThreads int, scale chan *frankenPHPContext,
}
// if the request has been stalled long enough, scale
if worker, ok := workers[getWorkerKey(fc.workerName, fc.scriptFilename)]; ok {
scaleWorkerThread(worker)
if fc.worker != nil {
scaleWorkerThread(fc.worker)
} else {
scaleRegularThread()
}

View File

@@ -48,7 +48,7 @@ func TestScaleAWorkerThreadUpAndDown(t *testing.T) {
autoScaledThread := phpThreads[2]
// scale up
scaleWorkerThread(workers[workerPath])
scaleWorkerThread(getWorkerByPath(workerPath))
assert.Equal(t, stateReady, autoScaledThread.state.get())
// on down-scale, the thread will be marked as inactive

View File

@@ -1 +1 @@
*.txt
test.txt

1
testdata/files/static.txt vendored Normal file
View File

@@ -0,0 +1 @@
Hello from file

3
testdata/hello.php vendored Normal file
View File

@@ -0,0 +1,3 @@
<?php
echo "Hello from PHP";

View File

@@ -21,27 +21,31 @@ type worker struct {
requestChan chan *frankenPHPContext
threads []*phpThread
threadMutex sync.RWMutex
allowPathMatching bool
maxConsecutiveFailures int
}
var (
workers map[string]*worker
workers []*worker
watcherIsEnabled bool
)
func initWorkers(opt []workerOpt) error {
workers = make(map[string]*worker, len(opt))
workers = make([]*worker, 0, len(opt))
workersReady := sync.WaitGroup{}
directoriesToWatch := getDirectoriesToWatch(opt)
watcherIsEnabled = len(directoriesToWatch) > 0
for _, o := range opt {
worker, err := newWorker(o)
w, err := newWorker(o)
if err != nil {
return err
}
workers = append(workers, w)
}
workersReady.Add(o.num)
for _, worker := range workers {
workersReady.Add(worker.num)
for i := 0; i < worker.num; i++ {
thread := getInactivePHPThread()
convertToWorkerThread(thread, worker)
@@ -66,12 +70,24 @@ func initWorkers(opt []workerOpt) error {
return nil
}
func getWorkerKey(name string, filename string) string {
key := filename
if strings.HasPrefix(name, "m#") {
key = name
func getWorkerByName(name string) *worker {
for _, w := range workers {
if w.name == name {
return w
}
}
return key
return nil
}
func getWorkerByPath(path string) *worker {
for _, w := range workers {
if w.fileName == path && w.allowPathMatching {
return w
}
}
return nil
}
func newWorker(o workerOpt) (*worker, error) {
@@ -80,24 +96,25 @@ func newWorker(o workerOpt) (*worker, error) {
return nil, fmt.Errorf("worker filename is invalid %q: %w", o.fileName, err)
}
key := getWorkerKey(o.name, absFileName)
if _, ok := workers[key]; ok {
return nil, fmt.Errorf("two workers cannot use the same key %q", key)
if o.name == "" {
o.name = absFileName
}
for _, w := range workers {
if w.name == o.name {
return w, fmt.Errorf("two workers cannot have the same name: %q", o.name)
}
// workers that have a name starting with "m#" are module workers
// they can only be matched by their name, not by their path
allowPathMatching := !strings.HasPrefix(o.name, "m#")
if w := getWorkerByPath(absFileName); w != nil && allowPathMatching {
return w, fmt.Errorf("two workers cannot have the same filename: %q", absFileName)
}
if w := getWorkerByName(o.name); w != nil {
return w, fmt.Errorf("two workers cannot have the same name: %q", o.name)
}
if o.env == nil {
o.env = make(PreparedEnv, 1)
}
if o.name == "" {
o.name = absFileName
}
o.env["FRANKENPHP_WORKER\x00"] = "1"
w := &worker{
name: o.name,
@@ -106,9 +123,9 @@ func newWorker(o workerOpt) (*worker, error) {
env: o.env,
requestChan: make(chan *frankenPHPContext),
threads: make([]*phpThread, 0, o.num),
allowPathMatching: allowPathMatching,
maxConsecutiveFailures: o.maxConsecutiveFailures,
}
workers[key] = w
return w, nil
}