Skip to content

Define domain specific workers in php_server and php blocks #1509

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

Open
wants to merge 53 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
a165c4e
add module (php_server directive) based workers
henderkes Apr 17, 2025
61bbef4
refactor moduleID to uintptr for faster comparisons
henderkes Apr 18, 2025
7fffbc2
let workers inherit environment variables and root from php_server
henderkes Apr 18, 2025
60c3e12
caddy can shift FrankenPHPModules in memory for some godforsaken reas…
henderkes Apr 18, 2025
97913f8
remove debugging statement
henderkes Apr 18, 2025
a22cd19
fix tests
henderkes Apr 18, 2025
a4016df
refactor moduleID to uint64 for faster comparisons
henderkes Apr 19, 2025
a21d3f4
actually allow multiple workers per script filename
henderkes Apr 19, 2025
7718a8e
remove logging
henderkes Apr 19, 2025
c7172d2
utility function
henderkes Apr 19, 2025
f955187
reuse existing worker with same filename and environment when calling…
henderkes Apr 19, 2025
e362fd3
no cleanup happens between tests, so restore old global worker overwr…
henderkes Apr 19, 2025
00d819f
add test, use getWorker(ForContext) function in frankenphp.go as well
henderkes Apr 20, 2025
bc48bdd
bring error on second global worker with the same filename again
henderkes Apr 20, 2025
0b22d51
refactor to using name instead of moduleID
henderkes Apr 21, 2025
c6bcacf
nicer name
henderkes Apr 21, 2025
53795c7
nicer name
henderkes Apr 21, 2025
958537e
add more tests
henderkes Apr 21, 2025
6c39229
remove test case already covered by previous test
henderkes Apr 21, 2025
e19b7b2
revert back to single variable, moduleIDs no longer relevant
henderkes Apr 21, 2025
2ff18ba
update comment
henderkes Apr 21, 2025
5fc1edf
figure out the worker to use in FrankenPHPModule::ServeHTTP
henderkes Apr 22, 2025
2c2f677
add caddy/config_tests, add --retry 5 to download
henderkes Apr 22, 2025
3b15199
add caddy/config_tests
henderkes Apr 22, 2025
af18d04
sum up logic a bit, put worker thread addition into moduleWorkers par…
henderkes Apr 22, 2025
dd5dc9b
implement suggestions as far as possible
henderkes Apr 23, 2025
c4937ac
fixup
henderkes Apr 23, 2025
401d25d
remove tags
henderkes Apr 23, 2025
a444361
feat: download the mostly static binary when possible (#1467)
dunglas Apr 18, 2025
6ebea60
docs: remove wildcard matcher from root directive (#1513)
IndraGunawan Apr 22, 2025
fb4e262
docs: update README with additional documentation links
Rom1Bastide Apr 22, 2025
9ff4ee2
ci: combine dependabot updates for one group to 1 pull-request
IndraGunawan Apr 22, 2025
e648532
feat: compatibility with libphp.dylib on macOS
dunglas Apr 22, 2025
af74391
feat: upgrade to Caddy 2.10
dunglas Apr 22, 2025
34f3b25
feat: upgrade to Caddy 2.10
dunglas Apr 22, 2025
9929383
chore: run prettier
dunglas Apr 22, 2025
0bbd4c6
fix: build-static.sh consecutive builds (#1496)
henderkes Apr 23, 2025
c96f53c
chore: update Go and toolchain version (#1526)
IndraGunawan Apr 23, 2025
26360fb
apply suggestions one be one - scriptpath only
henderkes Apr 24, 2025
39430e8
merge main into workers
henderkes Apr 24, 2025
801e71c
generate unique worker names by filename and number
henderkes Apr 24, 2025
6650045
support worker config from embedded apps
henderkes Apr 24, 2025
53e7bc0
rename back to make sure we don't accidentally add FrankenPHPApp work…
henderkes Apr 24, 2025
78be813
fix test after changing error message
henderkes Apr 24, 2025
4cc8893
use 🧩 for module workers
henderkes Apr 24, 2025
1c414ce
use 🌍 for global workers :)
henderkes Apr 24, 2025
9ccac16
Merge branch 'main' into workers
henderkes Apr 26, 2025
3f8f5ec
revert 1c414cebbc4380b26c4ac46a8662f88bd807aa09
henderkes Apr 26, 2025
a6596c7
revert 4cc8893cedc8a2c9e2195ca0e83e8e9cc359e136
henderkes Apr 27, 2025
d5d2eb3
apply suggestions
henderkes Apr 29, 2025
6f27895
add dynamic config loading test of module worker
henderkes Apr 29, 2025
a6b840f
Merge remote-tracking branch 'dunglas/main' into workers
henderkes Apr 29, 2025
877a6ce
fix test
henderkes Apr 29, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 140 additions & 8 deletions caddy/caddy.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
package caddy

import (
"crypto/sha256"
"encoding/binary"
"encoding/json"
"errors"
"fmt"
Expand All @@ -30,6 +32,12 @@ const defaultDocumentRoot = "public"

var iniError = errors.New("'php_ini' must be in the format: php_ini \"<key>\" \"<value>\"")

// sharedState is a package-level variable to store information that can be accessed by both FrankenPHPModule and FrankenPHPApp
var sharedState struct {
ModuleIDs []uint64
Workers []workerConfig
}

func init() {
caddy.RegisterModule(FrankenPHPApp{})
caddy.RegisterModule(FrankenPHPModule{})
Expand All @@ -55,6 +63,8 @@ type workerConfig struct {
Env map[string]string `json:"env,omitempty"`
// Directories to watch for file changes
Watch []string `json:"watch,omitempty"`
// ModuleID identifies which module created this worker
ModuleID uint64 `json:"module_id,omitempty"`
}

type FrankenPHPApp struct {
Expand Down Expand Up @@ -108,8 +118,14 @@ func (f *FrankenPHPApp) Start() error {
frankenphp.WithPhpIni(f.PhpIni),
frankenphp.WithMaxWaitTime(f.MaxWaitTime),
}
// Add workers from FrankenPHPApp configuration
for _, w := range f.Workers {
opts = append(opts, frankenphp.WithWorkers(w.Name, repl.ReplaceKnown(w.FileName, ""), w.Num, w.Env, w.Watch))
opts = append(opts, frankenphp.WithWorkers(w.Name, repl.ReplaceKnown(w.FileName, ""), w.Num, w.Env, w.Watch, w.ModuleID))
}

// Add workers from FrankenPHPModule configurations
for _, w := range sharedState.Workers {
opts = append(opts, frankenphp.WithWorkers(w.Name, repl.ReplaceKnown(w.FileName, ""), w.Num, w.Env, w.Watch, w.ModuleID))
}

frankenphp.Shutdown()
Expand All @@ -130,11 +146,14 @@ func (f *FrankenPHPApp) Stop() error {
frankenphp.DrainWorkers()
}

// reset configuration so it doesn't bleed into later tests
// reset the configuration so it doesn't bleed into later tests
f.Workers = nil
f.NumThreads = 0
f.MaxWaitTime = 0

// reset moduleWorkers
sharedState.Workers = nil

return nil
}

Expand Down Expand Up @@ -341,6 +360,10 @@ type FrankenPHPModule struct {
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"`
// ModuleID is the module ID that created this request.
ModuleID uint64 `json:"-"`
// Workers configures the worker scripts to start.
Workers []workerConfig `json:"workers,omitempty"`

resolvedDocumentRoot string
preparedEnv frankenphp.PreparedEnv
Expand Down Expand Up @@ -412,6 +435,21 @@ func (f *FrankenPHPModule) Provision(ctx caddy.Context) error {
}
}

if len(f.Workers) > 0 {
envString := ""
for k, v := range f.Env {
envString += k + "=" + v + ","
}
data := []byte(f.Root + envString)
hash := sha256.Sum256(data)
f.ModuleID = binary.LittleEndian.Uint64(hash[:8])

for i := range f.Workers {
f.Workers[i].ModuleID = f.ModuleID
}
sharedState.Workers = append(sharedState.Workers, f.Workers...)
}

return nil
}

Expand All @@ -422,7 +460,7 @@ func needReplacement(s string) bool {

// ServeHTTP implements caddyhttp.MiddlewareHandler.
// TODO: Expose TLS versions as env vars, as Apache's mod_ssl: https://github.com/caddyserver/caddy/blob/master/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go#L298
func (f FrankenPHPModule) ServeHTTP(w http.ResponseWriter, r *http.Request, _ caddyhttp.Handler) error {
func (f *FrankenPHPModule) ServeHTTP(w http.ResponseWriter, r *http.Request, _ caddyhttp.Handler) error {
origReq := r.Context().Value(caddyhttp.OriginalRequestCtxKey).(http.Request)
repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)

Expand All @@ -447,6 +485,7 @@ func (f FrankenPHPModule) ServeHTTP(w http.ResponseWriter, r *http.Request, _ ca
frankenphp.WithRequestSplitPath(f.SplitPath),
frankenphp.WithRequestPreparedEnv(env),
frankenphp.WithOriginalRequest(&origReq),
frankenphp.WithModuleID(f.ModuleID),
)

if err != nil {
Expand All @@ -462,7 +501,7 @@ func (f FrankenPHPModule) ServeHTTP(w http.ResponseWriter, r *http.Request, _ ca

// UnmarshalCaddyfile implements caddyfile.Unmarshaler.
func (f *FrankenPHPModule) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
// when adding a new directive, also update the allowedDirectives error message
// First pass: Parse all directives except "worker"
for d.Next() {
for d.NextBlock(0) {
switch d.Val() {
Expand Down Expand Up @@ -494,29 +533,121 @@ func (f *FrankenPHPModule) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
if !d.NextArg() {
continue
}

v, err := strconv.ParseBool(d.Val())
if err != nil {
return err
}
if d.NextArg() {
return d.ArgErr()
}

f.ResolveRootSymlink = &v

case "worker":
for d.NextBlock(1) {
}
for d.NextArg() {
}
// Skip "worker" blocks in the first pass
continue

default:
allowedDirectives := "root, split, env, resolve_root_symlink"
allowedDirectives := "root, split, env, resolve_root_symlink, worker"
return wrongSubDirectiveError("php or php_server", allowedDirectives, d.Val())
}
}
}

// Second pass: Parse only "worker" blocks
d.Reset()
for d.Next() {
for d.NextBlock(0) {
if d.Val() == "worker" {
wc := workerConfig{}
if d.NextArg() {
wc.FileName = d.Val()
}

if d.NextArg() {
v, err := strconv.Atoi(d.Val())
if err != nil {
return err
}
wc.Num = v
}

for d.NextBlock(1) {
switch d.Val() {
case "name":
if !d.NextArg() {
return d.ArgErr()
}
wc.Name = d.Val()
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]
case "watch":
if !d.NextArg() {
wc.Watch = append(wc.Watch, "./**/*.{php,yaml,yml,twig,env}")
} else {
wc.Watch = append(wc.Watch, d.Val())
}
default:
return fmt.Errorf("unknown worker subdirective: %s", d.Val())
}
}

if wc.FileName == "" {
return errors.New(`the "file" argument must be specified`)
}

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

f.Workers = append(f.Workers, wc)
}
}
}

return nil
}

// parseCaddyfile unmarshals tokens from h into a new Middleware.
func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {
m := FrankenPHPModule{}
m := &FrankenPHPModule{}
err := m.UnmarshalCaddyfile(h.Dispenser)

return m, err
Expand Down Expand Up @@ -753,6 +884,7 @@ func parsePhpServer(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error)
// using the php directive syntax
dispenser.Next() // consume the directive name
err = phpsrv.UnmarshalCaddyfile(dispenser)

if err != nil {
return nil, err
}
Expand Down
3 changes: 3 additions & 0 deletions context.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ type frankenPHPContext struct {

done chan interface{}
startedAt time.Time

// The module ID that created this request
moduleID uint64
}

// fromContext extracts the frankenPHPContext from a context.
Expand Down
13 changes: 9 additions & 4 deletions frankenphp.go
Original file line number Diff line number Diff line change
Expand Up @@ -405,12 +405,17 @@ func ServeHTTP(responseWriter http.ResponseWriter, request *http.Request) error
}

// Detect if a worker is available to handle this request
if worker, ok := workers[fc.scriptFilename]; ok {
worker.handleRequest(fc)
return nil
// Look for a worker with matching moduleID or a global worker (moduleID == 0)
if workersList, ok := workers[fc.scriptFilename]; ok {
for _, worker := range workersList {
if worker.moduleID == 0 || worker.moduleID == fc.moduleID {
worker.handleRequest(fc)
return nil
}
}
}

// If no worker was availabe send the request to non-worker threads
// If no worker was available, send the request to non-worker threads
handleRequestWithRegularPHPThreads(fc)

return nil
Expand Down
2 changes: 1 addition & 1 deletion frankenphp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,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("workerName", testDataDir+opts.workerScript, opts.nbWorkers, opts.env, opts.watch))
initOpts = append(initOpts, frankenphp.WithWorkers("workerName", testDataDir+opts.workerScript, opts.nbWorkers, opts.env, opts.watch, 0))
}
initOpts = append(initOpts, opts.initOpts...)
if opts.phpIni != nil {
Expand Down
7 changes: 4 additions & 3 deletions options.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ type workerOpt struct {
num int
env PreparedEnv
watch []string
moduleID uint64
}

// WithNumThreads configures the number of PHP threads to start.
Expand Down Expand Up @@ -55,10 +56,10 @@ func WithMetrics(m Metrics) Option {
}
}

// WithWorkers configures the PHP workers to start.
func WithWorkers(name string, fileName string, num int, env map[string]string, watch []string) Option {
// WithWorkers configures the PHP workers to start, moduleID is used to identify the worker for a specific domain
func WithWorkers(name string, fileName string, num int, env map[string]string, watch []string, moduleID uint64) Option {
return func(o *opt) error {
o.workers = append(o.workers, workerOpt{name, fileName, num, PrepareEnv(env), watch})
o.workers = append(o.workers, workerOpt{name, fileName, num, PrepareEnv(env), watch, moduleID})

return nil
}
Expand Down
10 changes: 5 additions & 5 deletions phpmainthread_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,8 @@ func TestTransitionThreadsWhileDoingRequests(t *testing.T) {

assert.NoError(t, Init(
WithNumThreads(numThreads),
WithWorkers(worker1Name, worker1Path, 1, map[string]string{"ENV1": "foo"}, []string{}),
WithWorkers(worker2Name, worker2Path, 1, map[string]string{"ENV1": "foo"}, []string{}),
WithWorkers(worker1Name, worker1Path, 1, map[string]string{"ENV1": "foo"}, []string{}, 0),
WithWorkers(worker2Name, worker2Path, 1, map[string]string{"ENV1": "foo"}, []string{}, 0),
WithLogger(zap.NewNop()),
))

Expand Down Expand Up @@ -179,7 +179,7 @@ func TestFinishBootingAWorkerScript(t *testing.T) {

func getDummyWorker(fileName string) *worker {
if workers == nil {
workers = make(map[string]*worker)
workers = make(map[string][]*worker)
}
worker, _ := newWorker(workerOpt{
fileName: testDataPath + "/" + fileName,
Expand Down Expand Up @@ -211,9 +211,9 @@ func allPossibleTransitions(worker1Path string, worker2Path string) []func(*phpT
thread.boot()
}
},
func(thread *phpThread) { convertToWorkerThread(thread, workers[worker1Path]) },
func(thread *phpThread) { convertToWorkerThread(thread, workers[worker1Path][0]) },
convertToInactiveThread,
func(thread *phpThread) { convertToWorkerThread(thread, workers[worker2Path]) },
func(thread *phpThread) { convertToWorkerThread(thread, workers[worker2Path][0]) },
convertToInactiveThread,
}
}
Expand Down
9 changes: 9 additions & 0 deletions request_options.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,3 +123,12 @@ func WithRequestLogger(logger *zap.Logger) RequestOption {
return nil
}
}

// WithModuleID sets the module ID associated with the current request
func WithModuleID(moduleID uint64) RequestOption {
return func(o *frankenPHPContext) error {
o.moduleID = moduleID

return nil
}
}
15 changes: 14 additions & 1 deletion scaling.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,19 @@ func scaleRegularThread() {
autoScaledThreads = append(autoScaledThreads, thread)
}

func getWorker(fc *frankenPHPContext) *worker {
// if the request has been stalled long enough, scale
if workersList, ok := workers[fc.scriptFilename]; ok {
// Look for a worker with matching moduleID or a global worker (moduleID == 0)
for _, worker := range workersList {
if worker.moduleID == 0 || worker.moduleID == fc.moduleID {
return worker
}
}
}
return nil
}

func startUpscalingThreads(maxScaledThreads int, scale chan *frankenPHPContext, done chan struct{}) {
for {
scalingMu.Lock()
Expand Down Expand Up @@ -160,7 +173,7 @@ func startUpscalingThreads(maxScaledThreads int, scale chan *frankenPHPContext,
}

// if the request has been stalled long enough, scale
if worker, ok := workers[fc.scriptFilename]; ok {
if worker := getWorker(fc); worker != nil {
scaleWorkerThread(worker)
} else {
scaleRegularThread()
Expand Down
Loading
Loading