Skip to content
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
56 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
94bce80
Merge branch 'main' into workers
henderkes May 3, 2025
73d3266
minor changes
dunglas May 5, 2025
1c38f0d
Merge branch 'main' into workers
dunglas May 5, 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
247 changes: 163 additions & 84 deletions caddy/caddy.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ const defaultDocumentRoot = "public"

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

// FrankenPHPModule instances register their workers and FrankenPHPApp reads them at Start() time
var moduleWorkers = make([]workerConfig, 0)

func init() {
caddy.RegisterModule(FrankenPHPApp{})
caddy.RegisterModule(FrankenPHPModule{})
Expand All @@ -45,7 +48,7 @@ func init() {
}

type workerConfig struct {
// Name for the worker
// Name for the worker. Default: the filename for FrankenPHPApp workers, always prefixed with "m#" for FrankenPHPModule workers.
Name string `json:"name,omitempty"`
// FileName sets the path to the worker script.
FileName string `json:"file_name,omitempty"`
Expand Down Expand Up @@ -108,7 +111,8 @@ func (f *FrankenPHPApp) Start() error {
frankenphp.WithPhpIni(f.PhpIni),
frankenphp.WithMaxWaitTime(f.MaxWaitTime),
}
for _, w := range f.Workers {
// Add workers from FrankenPHPApp and FrankenPHPModule configurations
for _, w := range append(f.Workers, moduleWorkers...) {
opts = append(opts, frankenphp.WithWorkers(w.Name, repl.ReplaceKnown(w.FileName, ""), w.Num, w.Env, w.Watch))
}

Expand All @@ -130,14 +134,95 @@ 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
moduleWorkers = nil

return nil
}

func parseWorkerConfig(d *caddyfile.Dispenser) (workerConfig, error) {
wc := workerConfig{}
if d.NextArg() {
wc.FileName = d.Val()
}

if d.NextArg() {
if d.Val() == "watch" {
wc.Watch = append(wc.Watch, "./**/*.{php,yaml,yml,twig,env}")
} else {
v, err := strconv.Atoi(d.Val())
Comment thread
henderkes marked this conversation as resolved.
Outdated
if err != nil {
return wc, err
}

wc.Num = v
}
}

if d.NextArg() {
return wc, errors.New("FrankenPHP: too many 'worker' arguments: " + d.Val())
Comment thread
henderkes marked this conversation as resolved.
Outdated
}

for d.NextBlock(1) {
v := d.Val()
switch v {
case "name":
if !d.NextArg() {
return wc, d.ArgErr()
}
wc.Name = d.Val()
case "file":
if !d.NextArg() {
return wc, d.ArgErr()
}
wc.FileName = d.Val()
case "num":
if !d.NextArg() {
return wc, d.ArgErr()
}

v, err := strconv.Atoi(d.Val())
Comment thread
henderkes marked this conversation as resolved.
Outdated
if err != nil {
return wc, err
}

wc.Num = v
case "env":
args := d.RemainingArgs()
if len(args) != 2 {
return wc, d.ArgErr()
}
if wc.Env == nil {
wc.Env = make(map[string]string)
}
wc.Env[args[0]] = args[1]
case "watch":
if !d.NextArg() {
// the default if the watch directory is left empty:
wc.Watch = append(wc.Watch, "./**/*.{php,yaml,yml,twig,env}")
} else {
wc.Watch = append(wc.Watch, d.Val())
}
default:
allowedDirectives := "name, file, num, env, watch"
return wc, wrongSubDirectiveError("worker", allowedDirectives, v)
}
}

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

if frankenphp.EmbeddedAppPath != "" && filepath.IsLocal(wc.FileName) {
wc.FileName = filepath.Join(frankenphp.EmbeddedAppPath, wc.FileName)
}

return wc, nil
}

// UnmarshalCaddyfile implements caddyfile.Unmarshaler.
func (f *FrankenPHPApp) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
for d.Next() {
Expand Down Expand Up @@ -219,82 +304,10 @@ func (f *FrankenPHPApp) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
}

case "worker":
wc := workerConfig{}
if d.NextArg() {
wc.FileName = d.Val()
}

if d.NextArg() {
if d.Val() == "watch" {
wc.Watch = append(wc.Watch, "./**/*.{php,yaml,yml,twig,env}")
} else {
v, err := strconv.Atoi(d.Val())
if err != nil {
return err
}

wc.Num = v
}
}

if d.NextArg() {
return errors.New("FrankenPHP: too many 'worker' arguments: " + d.Val())
}

for d.NextBlock(1) {
v := d.Val()
switch v {
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() {
// the default if the watch directory is left empty:
wc.Watch = append(wc.Watch, "./**/*.{php,yaml,yml,twig,env}")
} else {
wc.Watch = append(wc.Watch, d.Val())
}
default:
allowedDirectives := "name, file, num, env, watch"
return wrongSubDirectiveError("worker", allowedDirectives, v)
}
}

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

if frankenphp.EmbeddedAppPath != "" && filepath.IsLocal(wc.FileName) {
wc.FileName = filepath.Join(frankenphp.EmbeddedAppPath, wc.FileName)
wc, err := parseWorkerConfig(d)
if err != nil {
return err
}

if wc.Name == "" {
// let worker initialization validate if the FileName is valid or not
name, _ := fastabs.FastAbs(wc.FileName)
Expand Down Expand Up @@ -341,6 +354,8 @@ 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"`
// Workers configures the worker scripts to start.
Workers []workerConfig `json:"workers,omitempty"`

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

if len(f.Workers) > 0 {
moduleWorkers = append(moduleWorkers, f.Workers...)
}

return nil
}

Expand All @@ -422,7 +441,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 @@ -441,12 +460,18 @@ func (f FrankenPHPModule) ServeHTTP(w http.ResponseWriter, r *http.Request, _ ca
}
}

workerNames := make([]string, len(f.Workers))
for i, w := range f.Workers {
workerNames[i] = w.Name
}

fr, err := frankenphp.NewRequestWithContext(
r,
documentRootOption,
frankenphp.WithRequestSplitPath(f.SplitPath),
frankenphp.WithRequestPreparedEnv(env),
frankenphp.WithOriginalRequest(&origReq),
frankenphp.WithWorkerNames(workerNames),
)

if err != nil {
Expand All @@ -462,7 +487,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 +519,82 @@ 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":
Comment thread
henderkes marked this conversation as resolved.
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) {
Comment thread
henderkes marked this conversation as resolved.
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)
}
Comment thread
henderkes marked this conversation as resolved.

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

if wc.Name == "" && len(wc.Env) > 0 {
if len(wc.Env) > 0 {
envString := ""
for k, v := range wc.Env {
envString += k + "=" + v + ","
}
envString = strings.TrimSuffix(envString, ",")
wc.Name += "env:" + envString + "_" + wc.FileName
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would probably also be enough to just have a func that looks through all workers and adds a number to the end until the name is unique.

@henderkes henderkes Apr 21, 2025

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, together with the prefix that should work. Good idea.

I used this however to default to a single worker for different modules if they contain the same environment variables. So instead of moduleWorker1 with 1 thread and moduleWorker2 with 1 thread, there'd be moduleWorker:env:_filename with 2 threads.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I should refactor the f.Workers array out of FrankenPHPApp and FrankenPHPModule and instead only keep a single instace that's updated from both places. Then I could check for existence of a suitable worker even if it isn't in the name.

Or I just don't care about duplicate suitable workers and leave it up to the user to define shared workers in a way that makes sense. Maybe that's the better approach.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And FrankenPHPModule would be responsible for finding a suitable worker, the option would be WithWorker instead of WithWorkerNames.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@AlliBalliBaba Hah, I did it, but it makes tests fail because they can't ensure the config was correctly loaded, as the numbers won't match in the name. I'm therefore going back to the environment strings. It's just an auto-generated name to keep the worker names unique, anyhow.

if wc.Name != "" && !strings.HasPrefix(wc.Name, "m#") {
wc.Name = "m#" + wc.Name
}

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 +831,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
Loading