Skip to content

Commit 82aeb12

Browse files
refactor: split caddy.go (#1629)
* Splits modules. * trigger build --------- Co-authored-by: Alliballibaba <alliballibaba@gmail.com>
1 parent 6749ddb commit 82aeb12

4 files changed

Lines changed: 961 additions & 900 deletions

File tree

caddy/app.go

Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
package caddy
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"log/slog"
7+
"path/filepath"
8+
"strconv"
9+
"strings"
10+
"time"
11+
12+
"github.com/caddyserver/caddy/v2"
13+
"github.com/caddyserver/caddy/v2/caddyconfig"
14+
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
15+
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
16+
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
17+
"github.com/dunglas/frankenphp"
18+
"github.com/dunglas/frankenphp/internal/fastabs"
19+
)
20+
21+
// FrankenPHPApp represents the global "frankenphp" directive in the Caddyfile
22+
// it's responsible for starting up the global PHP instance and all threads
23+
//
24+
// {
25+
// frankenphp {
26+
// num_threads 20
27+
// }
28+
// }
29+
type FrankenPHPApp struct {
30+
// NumThreads sets the number of PHP threads to start. Default: 2x the number of available CPUs.
31+
NumThreads int `json:"num_threads,omitempty"`
32+
// MaxThreads limits how many threads can be started at runtime. Default 2x NumThreads
33+
MaxThreads int `json:"max_threads,omitempty"`
34+
// Workers configures the worker scripts to start.
35+
Workers []workerConfig `json:"workers,omitempty"`
36+
// Overwrites the default php ini configuration
37+
PhpIni map[string]string `json:"php_ini,omitempty"`
38+
// The maximum amount of time a request may be stalled waiting for a thread
39+
MaxWaitTime time.Duration `json:"max_wait_time,omitempty"`
40+
41+
metrics frankenphp.Metrics
42+
logger *slog.Logger
43+
}
44+
45+
var iniError = errors.New("'php_ini' must be in the format: php_ini \"<key>\" \"<value>\"")
46+
47+
// CaddyModule returns the Caddy module information.
48+
func (f FrankenPHPApp) CaddyModule() caddy.ModuleInfo {
49+
return caddy.ModuleInfo{
50+
ID: "frankenphp",
51+
New: func() caddy.Module { return &f },
52+
}
53+
}
54+
55+
// Provision sets up the module.
56+
func (f *FrankenPHPApp) Provision(ctx caddy.Context) error {
57+
f.logger = ctx.Slogger()
58+
59+
if httpApp, err := ctx.AppIfConfigured("http"); err == nil {
60+
if httpApp.(*caddyhttp.App).Metrics != nil {
61+
f.metrics = frankenphp.NewPrometheusMetrics(ctx.GetMetricsRegistry())
62+
}
63+
} else {
64+
// if the http module is not configured (this should never happen) then collect the metrics by default
65+
f.metrics = frankenphp.NewPrometheusMetrics(ctx.GetMetricsRegistry())
66+
}
67+
68+
return nil
69+
}
70+
71+
func (f *FrankenPHPApp) generateUniqueModuleWorkerName(filepath string) string {
72+
var i uint
73+
filepath, _ = fastabs.FastAbs(filepath)
74+
name := "m#" + filepath
75+
76+
retry:
77+
for _, wc := range f.Workers {
78+
if wc.Name == name {
79+
name = fmt.Sprintf("m#%s_%d", filepath, i)
80+
i++
81+
82+
goto retry
83+
}
84+
}
85+
86+
return name
87+
}
88+
89+
func (f *FrankenPHPApp) addModuleWorkers(workers ...workerConfig) ([]workerConfig, error) {
90+
for i := range workers {
91+
w := &workers[i]
92+
if frankenphp.EmbeddedAppPath != "" && filepath.IsLocal(w.FileName) {
93+
w.FileName = filepath.Join(frankenphp.EmbeddedAppPath, w.FileName)
94+
}
95+
if w.Name == "" {
96+
w.Name = f.generateUniqueModuleWorkerName(w.FileName)
97+
} else if !strings.HasPrefix(w.Name, "m#") {
98+
w.Name = "m#" + w.Name
99+
}
100+
f.Workers = append(f.Workers, *w)
101+
}
102+
return workers, nil
103+
}
104+
105+
func (f *FrankenPHPApp) Start() error {
106+
repl := caddy.NewReplacer()
107+
108+
opts := []frankenphp.Option{
109+
frankenphp.WithNumThreads(f.NumThreads),
110+
frankenphp.WithMaxThreads(f.MaxThreads),
111+
frankenphp.WithLogger(f.logger),
112+
frankenphp.WithMetrics(f.metrics),
113+
frankenphp.WithPhpIni(f.PhpIni),
114+
frankenphp.WithMaxWaitTime(f.MaxWaitTime),
115+
}
116+
for _, w := range append(f.Workers) {
117+
opts = append(opts, frankenphp.WithWorkers(w.Name, repl.ReplaceKnown(w.FileName, ""), w.Num, w.Env, w.Watch))
118+
}
119+
120+
frankenphp.Shutdown()
121+
if err := frankenphp.Init(opts...); err != nil {
122+
return err
123+
}
124+
125+
return nil
126+
}
127+
128+
func (f *FrankenPHPApp) Stop() error {
129+
f.logger.Info("FrankenPHP stopped 🐘")
130+
131+
// attempt a graceful shutdown if caddy is exiting
132+
// note: Exiting() is currently marked as 'experimental'
133+
// https://github.com/caddyserver/caddy/blob/e76405d55058b0a3e5ba222b44b5ef00516116aa/caddy.go#L810
134+
if caddy.Exiting() {
135+
frankenphp.DrainWorkers()
136+
}
137+
138+
// reset the configuration so it doesn't bleed into later tests
139+
f.Workers = nil
140+
f.NumThreads = 0
141+
f.MaxWaitTime = 0
142+
143+
return nil
144+
}
145+
146+
// UnmarshalCaddyfile implements caddyfile.Unmarshaler.
147+
func (f *FrankenPHPApp) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
148+
for d.Next() {
149+
for d.NextBlock(0) {
150+
// when adding a new directive, also update the allowedDirectives error message
151+
switch d.Val() {
152+
case "num_threads":
153+
if !d.NextArg() {
154+
return d.ArgErr()
155+
}
156+
157+
v, err := strconv.ParseUint(d.Val(), 10, 32)
158+
if err != nil {
159+
return err
160+
}
161+
162+
f.NumThreads = int(v)
163+
case "max_threads":
164+
if !d.NextArg() {
165+
return d.ArgErr()
166+
}
167+
168+
if d.Val() == "auto" {
169+
f.MaxThreads = -1
170+
continue
171+
}
172+
173+
v, err := strconv.ParseUint(d.Val(), 10, 32)
174+
if err != nil {
175+
return err
176+
}
177+
178+
f.MaxThreads = int(v)
179+
case "max_wait_time":
180+
if !d.NextArg() {
181+
return d.ArgErr()
182+
}
183+
184+
v, err := time.ParseDuration(d.Val())
185+
if err != nil {
186+
return errors.New("max_wait_time must be a valid duration (example: 10s)")
187+
}
188+
189+
f.MaxWaitTime = v
190+
case "php_ini":
191+
parseIniLine := func(d *caddyfile.Dispenser) error {
192+
key := d.Val()
193+
if !d.NextArg() {
194+
return iniError
195+
}
196+
if f.PhpIni == nil {
197+
f.PhpIni = make(map[string]string)
198+
}
199+
f.PhpIni[key] = d.Val()
200+
if d.NextArg() {
201+
return iniError
202+
}
203+
204+
return nil
205+
}
206+
207+
isBlock := false
208+
for d.NextBlock(1) {
209+
isBlock = true
210+
err := parseIniLine(d)
211+
if err != nil {
212+
return err
213+
}
214+
}
215+
216+
if !isBlock {
217+
if !d.NextArg() {
218+
return iniError
219+
}
220+
err := parseIniLine(d)
221+
if err != nil {
222+
return err
223+
}
224+
}
225+
226+
case "worker":
227+
wc, err := parseWorkerConfig(d)
228+
if err != nil {
229+
return err
230+
}
231+
if frankenphp.EmbeddedAppPath != "" && filepath.IsLocal(wc.FileName) {
232+
wc.FileName = filepath.Join(frankenphp.EmbeddedAppPath, wc.FileName)
233+
}
234+
if strings.HasPrefix(wc.Name, "m#") {
235+
return fmt.Errorf(`global worker names must not start with "m#": %q`, wc.Name)
236+
}
237+
// check for duplicate workers
238+
for _, existingWorker := range f.Workers {
239+
if existingWorker.FileName == wc.FileName {
240+
return fmt.Errorf("global workers must not have duplicate filenames: %q", wc.FileName)
241+
}
242+
}
243+
244+
f.Workers = append(f.Workers, wc)
245+
default:
246+
allowedDirectives := "num_threads, max_threads, php_ini, worker, max_wait_time"
247+
return wrongSubDirectiveError("frankenphp", allowedDirectives, d.Val())
248+
}
249+
}
250+
}
251+
252+
if f.MaxThreads > 0 && f.NumThreads > 0 && f.MaxThreads < f.NumThreads {
253+
return errors.New(`"max_threads"" must be greater than or equal to "num_threads"`)
254+
}
255+
256+
return nil
257+
}
258+
259+
func parseGlobalOption(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
260+
app := &FrankenPHPApp{}
261+
if err := app.UnmarshalCaddyfile(d); err != nil {
262+
return nil, err
263+
}
264+
265+
// tell Caddyfile adapter that this is the JSON for an app
266+
return httpcaddyfile.App{
267+
Name: "frankenphp",
268+
Value: caddyconfig.JSON(app, nil),
269+
}, nil
270+
}

0 commit comments

Comments
 (0)