Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
746 changes: 746 additions & 0 deletions bgworker_test.go

Large diffs are not rendered by default.

11 changes: 7 additions & 4 deletions caddy/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -410,10 +410,13 @@ func (f *FrankenPHPApp) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
if len(wc.MatchPath) != 0 {
return d.Errf(`"match" can only be used in a php_server worker block, not in a global one: %q`, wc.FileName)
}
// check for duplicate workers
for _, existingWorker := range f.Workers {
if existingWorker.FileName == wc.FileName {
return d.Errf("global workers must not have duplicate filenames: %q", wc.FileName)
// check for duplicate workers; background workers are not
// matched by path, several may share a script
if !wc.Background {
for _, existingWorker := range f.Workers {
if !existingWorker.Background && existingWorker.FileName == wc.FileName {
return d.Errf("global workers must not have duplicate filenames: %q", wc.FileName)
}
}
}

Expand Down
51 changes: 45 additions & 6 deletions caddy/caddy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -788,6 +788,45 @@ func TestMetrics(t *testing.T) {
require.NoError(t, testutil.GatherAndCompare(ctx.GetMetricsRegistry(), strings.NewReader(expectedMetrics), "frankenphp_total_threads", "frankenphp_busy_threads"))
}

// TestBackgroundWorkerFromCaddyfile starts a background worker from a
// Caddyfile and checks it runs: the sentinel its script touches appears
func TestBackgroundWorkerFromCaddyfile(t *testing.T) {
sentinel := filepath.ToSlash(filepath.Join(t.TempDir(), "bg.sentinel"))
tester := caddytest.NewTester(t)
initServer(t, tester, `
{
skip_install_trust
admin localhost:2999
http_port `+testPort+`
https_port 9443

frankenphp {
worker {
file ../testdata/bgworker/basic.php
num 1
name bg-caddy
background
env BG_SENTINEL `+sentinel+`
}
}
}

localhost:`+testPort+` {
route {
php {
root ../testdata
}
}
}
`, "caddyfile")

require.Eventually(t, func() bool {
_, err := os.Stat(sentinel)

return err == nil
}, 5*time.Second, 25*time.Millisecond, "the background worker declared in the Caddyfile did not run")
}

func TestWorkerMetrics(t *testing.T) {
var wg sync.WaitGroup
tester := caddytest.NewTester(t)
Expand Down Expand Up @@ -870,7 +909,7 @@ func TestWorkerMetrics(t *testing.T) {
# TYPE frankenphp_worker_request_count counter
frankenphp_worker_request_count{worker="` + workerName + `"} 10

# HELP frankenphp_ready_workers Running workers that have successfully called frankenphp_handle_request at least once
# HELP frankenphp_ready_workers Running workers that have reached their ready point at least once: frankenphp_handle_request for HTTP workers, WorkerHandle::tick() for background workers
# TYPE frankenphp_ready_workers gauge
frankenphp_ready_workers{worker="` + workerName + `"} 2
`
Expand Down Expand Up @@ -1027,7 +1066,7 @@ func TestNamedWorkerMetrics(t *testing.T) {
# TYPE frankenphp_worker_request_count counter
frankenphp_worker_request_count{worker="my_app"} 10

# HELP frankenphp_ready_workers Running workers that have successfully called frankenphp_handle_request at least once
# HELP frankenphp_ready_workers Running workers that have reached their ready point at least once: frankenphp_handle_request for HTTP workers, WorkerHandle::tick() for background workers
# TYPE frankenphp_ready_workers gauge
frankenphp_ready_workers{worker="my_app"} 2
`
Expand Down Expand Up @@ -1123,7 +1162,7 @@ func TestAutoWorkerConfig(t *testing.T) {
# TYPE frankenphp_worker_request_count counter
frankenphp_worker_request_count{worker="` + workerName + `"} 10

# HELP frankenphp_ready_workers Running workers that have successfully called frankenphp_handle_request at least once
# HELP frankenphp_ready_workers Running workers that have reached their ready point at least once: frankenphp_handle_request for HTTP workers, WorkerHandle::tick() for background workers
# TYPE frankenphp_ready_workers gauge
frankenphp_ready_workers{worker="` + workerName + `"} ` + workers + `
`
Expand Down Expand Up @@ -1498,7 +1537,7 @@ func TestMultiWorkersMetrics(t *testing.T) {
# TYPE frankenphp_worker_request_count counter
frankenphp_worker_request_count{worker="service1"} 10

# HELP frankenphp_ready_workers Running workers that have successfully called frankenphp_handle_request at least once
# HELP frankenphp_ready_workers Running workers that have reached their ready point at least once: frankenphp_handle_request for HTTP workers, WorkerHandle::tick() for background workers
# TYPE frankenphp_ready_workers gauge
frankenphp_ready_workers{worker="service1"} 2
frankenphp_ready_workers{worker="service2"} 3
Expand Down Expand Up @@ -1652,7 +1691,7 @@ func TestWorkerRestart(t *testing.T) {

// Check metrics
expectedMetrics := `
# HELP frankenphp_ready_workers Running workers that have successfully called frankenphp_handle_request at least once
# HELP frankenphp_ready_workers Running workers that have reached their ready point at least once: frankenphp_handle_request for HTTP workers, WorkerHandle::tick() for background workers
# TYPE frankenphp_ready_workers gauge
frankenphp_ready_workers{worker="service"} 1
# HELP frankenphp_total_workers Total number of PHP workers for this worker
Expand Down Expand Up @@ -1680,7 +1719,7 @@ func TestWorkerRestart(t *testing.T) {

// frankenphp_ready_workers should be back to 1 even after worker restarts
expectedMetrics = `
# HELP frankenphp_ready_workers Running workers that have successfully called frankenphp_handle_request at least once
# HELP frankenphp_ready_workers Running workers that have reached their ready point at least once: frankenphp_handle_request for HTTP workers, WorkerHandle::tick() for background workers
# TYPE frankenphp_ready_workers gauge
frankenphp_ready_workers{worker="service"} 1
# HELP frankenphp_total_workers Total number of PHP workers for this worker
Expand Down
187 changes: 187 additions & 0 deletions caddy/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,45 @@ func TestModuleWorkerDuplicateFilenamesFail(t *testing.T) {
require.Contains(t, err.Error(), "must not have duplicate filenames", "Error message should mention duplicate filenames")
}

// two global background workers may share a script, like their php_server
// counterparts and the Go API: they are not matched by path
func TestGlobalBackgroundWorkersShareAFilename(t *testing.T) {
d := caddyfile.NewTestDispenser(`
{
frankenphp {
worker {
name first
file ../testdata/worker-with-env.php
num 1
background
}
worker {
name second
file ../testdata/worker-with-env.php
num 1
background
}
}
}`)
app := &FrankenPHPApp{}

require.NoError(t, app.UnmarshalCaddyfile(d))
require.Len(t, app.Workers, 2)
}

func TestGlobalWorkerDuplicateFilenamesFail(t *testing.T) {
d := caddyfile.NewTestDispenser(`
{
frankenphp {
worker ../testdata/worker-with-env.php
worker ../testdata/worker-with-env.php
}
}`)
app := &FrankenPHPApp{}

require.ErrorContains(t, app.UnmarshalCaddyfile(d), "must not have duplicate filenames")
}

func TestModuleWorkersWithDifferentFilenames(t *testing.T) {
// Create a test configuration with different worker filenames
configWithDifferentFilenames := `
Expand Down Expand Up @@ -295,3 +334,151 @@ func TestCreateUniqueWorkerNamesQualifiedByServer(t *testing.T) {
// workers without a server keep the numeric postfix behavior
require.Equal(t, "queue_2", createUniqueWorkerName(usedWorkerNames, wc, ""))
}

func TestWorkerBackgroundConfig(t *testing.T) {
d := caddyfile.NewTestDispenser(`
{
php_server {
worker {
name jobs
file ../testdata/worker-with-env.php
num 2
background
}
}
}`)
module := &FrankenPHPModule{}

require.NoError(t, module.UnmarshalCaddyfile(d))
require.Len(t, module.Workers, 1)
require.True(t, module.Workers[0].Background)
require.Equal(t, "jobs", module.Workers[0].Name)
}

// the name is generated when the workers are collected, like for an HTTP worker
func TestWorkerBackgroundNameIsOptional(t *testing.T) {
d := caddyfile.NewTestDispenser(`
{
php_server {
worker {
file ../testdata/worker-with-env.php
background
}
}
}`)
module := &FrankenPHPModule{}

require.NoError(t, module.UnmarshalCaddyfile(d))
require.True(t, module.Workers[0].Background)
require.Empty(t, module.Workers[0].Name)
}

func TestWorkerBackgroundVars(t *testing.T) {
d := caddyfile.NewTestDispenser(`
{
php_server {
worker {
file ../testdata/worker-with-env.php
background
vars redis feature_flags
}
}
}`)
module := &FrankenPHPModule{}

require.NoError(t, module.UnmarshalCaddyfile(d))
require.Equal(t, []string{"redis", "feature_flags"}, module.Workers[0].Vars)
}

func TestWorkerVarsRequireBackground(t *testing.T) {
d := caddyfile.NewTestDispenser(`
{
php_server {
worker {
file ../testdata/worker-with-env.php
vars redis
}
}
}`)
module := &FrankenPHPModule{}

require.ErrorContains(t, module.UnmarshalCaddyfile(d), `"vars" is only supported for background workers`)
}

func TestWorkerBackgroundWithoutNumParses(t *testing.T) {
d := caddyfile.NewTestDispenser(`
{
php_server {
worker {
name jobs
file ../testdata/worker-with-env.php
background
}
}
}`)
module := &FrankenPHPModule{}

// num is optional, it defaults to one thread when the workers start
require.NoError(t, module.UnmarshalCaddyfile(d))
}

func TestWorkerBootTimeout(t *testing.T) {
parse := func(t *testing.T, directives string) (*FrankenPHPModule, error) {
t.Helper()
module := &FrankenPHPModule{}
err := module.UnmarshalCaddyfile(caddyfile.NewTestDispenser(`
{
php_server {
worker {
file ../testdata/worker-with-env.php
` + directives + `
}
}
}`))

return module, err
}

module, err := parse(t, "background\nboot_timeout 5s")
require.NoError(t, err)
require.Equal(t, caddy.Duration(5*time.Second), *module.Workers[0].BootTimeout)

// zero waits forever
module, err = parse(t, "background\nboot_timeout 0")
require.NoError(t, err)
require.Equal(t, caddy.Duration(0), *module.Workers[0].BootTimeout)

_, err = parse(t, "boot_timeout 5s")
require.ErrorContains(t, err, `"boot_timeout" is only supported for background workers`)

_, err = parse(t, "background\nboot_timeout -1s")
require.ErrorContains(t, err, "boot_timeout must be >= 0")
}

func TestWorkerBackgroundRejectsMatch(t *testing.T) {
d := caddyfile.NewTestDispenser(`
{
php_server {
worker {
name jobs
file ../testdata/worker-with-env.php
match /jobs/*
background
}
}
}`)
module := &FrankenPHPModule{}

err := module.UnmarshalCaddyfile(d)
require.ErrorContains(t, err, `"match" is not supported for background workers`)
}

// a JSON config skips the Caddyfile check: the core refuses vars on an HTTP
// worker instead of the option being dropped
func TestWorkerVarsReachTheCoreWithoutBackground(t *testing.T) {
wc := workerConfig{FileName: "../testdata/worker-with-env.php", Vars: []string{"cfg"}}
opts, err := wc.toWorkerOptions()
require.NoError(t, err)

require.ErrorContains(t, frankenphp.Validate(frankenphp.WithWorkers("http", wc.FileName, 1, opts...)), "only background workers publish them")
}
4 changes: 4 additions & 0 deletions caddy/module.go
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,10 @@ func (f *FrankenPHPModule) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
// 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 {
// background workers are not matched by path, several may share a script
if w.Background {
continue
}
if _, ok := fileNames[w.FileName]; ok {
return fmt.Errorf(`workers in a single "php" or "php_server" block must not have duplicate filenames: %q`, w.FileName)
}
Expand Down
Loading
Loading