@marcel91200 / caddy Mirror · read-only / commits / 769ed7e4
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 11, 06:00 UTC

events: skip dispatch setup when nothing is subscribed (#7997)

App.Emit does a fair amount of work before it can discover that no handler is bound: it derives three loggers, one of which formats the event's UUID even when debug logging is off, and registers a replacer callback. Only then does it reach "shortcut if event not bound at all". Some events are emitted on every TLS handshake -- CertMagic emits tls_get_certificate as the first statement of GetCertificateWithContext -- so on a server with no events configuration that work runs per handshake and is discarded every time. Return early when neither the event's name nor the catch-all is bound and debug logging is off, which are exactly the conditions under which nothing can observe the event. caddy.NewEvent still runs, so the returned Event is unchanged for callers. Benchmarks are included; measurements are in the pull request.

Y.Horie committed Sep 9, 2026 at 12:33 UTC 769ed7e4a33e76c529d1a2446bd9817113e910e7
3 files changed +266 -3
modules/caddyevents/app.go
+16 -3
@@ -22,6 +22,7 @@ import (
22 "strings"
23
24 "go.uber.org/zap"
25 + "go.uber.org/zap/zapcore"
26
27 "github.com/caddyserver/caddy/v2"
28 )
@@ -205,13 +206,25 @@ func (app *App) On(eventName string, handler Handler) error {
206 // Note that the data map is not copied, for efficiency. After Emit() is called, the
207 // data passed in should not be changed in other goroutines.
208 func (app *App) Emit(ctx caddy.Context, eventName string, data map[string]any) caddy.Event {
208 - logger := app.logger.With(zap.String("name", eventName))
209 -
209 e, err := caddy.NewEvent(ctx, eventName, data)
210 if err != nil {
212 - logger.Error("failed to create event", zap.Error(err))
211 + app.logger.Error("failed to create event",
212 + zap.String("name", eventName), zap.Error(err))
213 + }
214 +
215 + // A handler can only be reached through subscriptions to this event by
216 + // name or to all events, so if neither is bound, nothing can observe
217 + // this event and the only remaining output is the debug log below.
218 + // Bail out before deriving loggers and registering replacer values:
219 + // some events, such as tls_get_certificate, are emitted on every TLS
220 + // handshake, where that work is significant and always wasted.
221 + if app.subscriptions[eventName] == nil && app.subscriptions[""] == nil &&
222 + !app.logger.Core().Enabled(zapcore.DebugLevel) {
223 + return e
224 }
225
226 + logger := app.logger.With(zap.String("name", eventName))
227 +
228 var originModule caddy.ModuleInfo
229 var originModuleID caddy.ModuleID
230 var originModuleName string
modules/caddyevents/app_test.go new
+139
@@ -0,0 +1,139 @@
1 +// Copyright 2015 Matthew Holt and The Caddy Authors
2 +//
3 +// Licensed under the Apache License, Version 2.0 (the "License");
4 +// you may not use this file except in compliance with the License.
5 +// You may obtain a copy of the License at
6 +//
7 +// http://www.apache.org/licenses/LICENSE-2.0
8 +//
9 +// Unless required by applicable law or agreed to in writing, software
10 +// distributed under the License is distributed on an "AS IS" BASIS,
11 +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 +// See the License for the specific language governing permissions and
13 +// limitations under the License.
14 +
15 +package caddyevents
16 +
17 +import (
18 + "context"
19 + "io"
20 + "testing"
21 +
22 + "go.uber.org/zap"
23 + "go.uber.org/zap/zapcore"
24 +
25 + "github.com/caddyserver/caddy/v2"
26 +)
27 +
28 +// discardLogger stands in for a production logger: a real core at info level,
29 +// so debug output is filtered but everything else behaves normally.
30 +func discardLogger() *zap.Logger {
31 + return zap.New(zapcore.NewCore(
32 + zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()),
33 + zapcore.AddSync(io.Discard),
34 + zapcore.InfoLevel,
35 + ))
36 +}
37 +
38 +func testApp(tb testing.TB) (*App, caddy.Context, context.CancelFunc) {
39 + tb.Helper()
40 + app := &App{
41 + logger: discardLogger(),
42 + subscriptions: make(map[string]map[caddy.ModuleID][]Handler),
43 + }
44 + ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
45 + return app, ctx, cancel
46 +}
47 +
48 +type countingHandler struct{ count int }
49 +
50 +func (h *countingHandler) Handle(context.Context, caddy.Event) error {
51 + h.count++
52 + return nil
53 +}
54 +
55 +// Emit takes a shortcut when nothing is subscribed; a handler bound to the
56 +// event by name must still be invoked.
57 +func TestEmitDispatchesToNamedSubscriber(t *testing.T) {
58 + app, ctx, cancel := testApp(t)
59 + defer cancel()
60 +
61 + h := new(countingHandler)
62 + if err := app.On("cert_obtained", h); err != nil {
63 + t.Fatal(err)
64 + }
65 +
66 + app.Emit(ctx, "cert_obtained", nil)
67 + if h.count != 1 {
68 + t.Errorf("handler invoked %d times, want 1", h.count)
69 + }
70 +
71 + // an event nobody subscribed to must not reach it
72 + app.Emit(ctx, "cert_failed", nil)
73 + if h.count != 1 {
74 + t.Errorf("handler invoked %d times after unrelated event, want 1", h.count)
75 + }
76 +}
77 +
78 +// Subscribing without naming an event binds to every event, which is stored
79 +// under the empty event name; the shortcut has to account for that.
80 +func TestEmitDispatchesToCatchAllSubscriber(t *testing.T) {
81 + app, ctx, cancel := testApp(t)
82 + defer cancel()
83 +
84 + h := new(countingHandler)
85 + if err := app.Subscribe(&Subscription{Handlers: []Handler{h}}); err != nil {
86 + t.Fatal(err)
87 + }
88 +
89 + app.Emit(ctx, "tls_get_certificate", nil)
90 + if h.count != 1 {
91 + t.Errorf("handler invoked %d times, want 1", h.count)
92 + }
93 +}
94 +
95 +// Some events, such as tls_get_certificate, are emitted on every TLS
96 +// handshake, whether or not anything is subscribed to them.
97 +func BenchmarkEmitNoSubscribers(b *testing.B) {
98 + app, ctx, cancel := testApp(b)
99 + defer cancel()
100 +
101 + data := map[string]any{"client_hello": "example.com"}
102 +
103 + b.ReportAllocs()
104 + b.ResetTimer()
105 + for i := 0; i < b.N; i++ {
106 + app.Emit(ctx, "tls_get_certificate", data)
107 + }
108 +}
109 +
110 +func BenchmarkEmitNoSubscribersParallel(b *testing.B) {
111 + app, ctx, cancel := testApp(b)
112 + defer cancel()
113 +
114 + data := map[string]any{"client_hello": "example.com"}
115 +
116 + b.ReportAllocs()
117 + b.ResetTimer()
118 + b.RunParallel(func(pb *testing.PB) {
119 + for pb.Next() {
120 + app.Emit(ctx, "tls_get_certificate", data)
121 + }
122 + })
123 +}
124 +
125 +func BenchmarkEmitWithSubscriber(b *testing.B) {
126 + app, ctx, cancel := testApp(b)
127 + defer cancel()
128 +
129 + if err := app.On("tls_get_certificate", new(countingHandler)); err != nil {
130 + b.Fatal(err)
131 + }
132 + data := map[string]any{"client_hello": "example.com"}
133 +
134 + b.ReportAllocs()
135 + b.ResetTimer()
136 + for i := 0; i < b.N; i++ {
137 + app.Emit(ctx, "tls_get_certificate", data)
138 + }
139 +}
modules/caddyevents/handshake_bench_test.go new
+111
@@ -0,0 +1,111 @@
1 +// Copyright 2015 Matthew Holt and The Caddy Authors
2 +//
3 +// Licensed under the Apache License, Version 2.0 (the "License");
4 +// you may not use this file except in compliance with the License.
5 +// You may obtain a copy of the License at
6 +//
7 +// http://www.apache.org/licenses/LICENSE-2.0
8 +//
9 +// Unless required by applicable law or agreed to in writing, software
10 +// distributed under the License is distributed on an "AS IS" BASIS,
11 +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 +// See the License for the specific language governing permissions and
13 +// limitations under the License.
14 +
15 +package caddyevents
16 +
17 +import (
18 + "context"
19 + "crypto/ecdsa"
20 + "crypto/elliptic"
21 + "crypto/rand"
22 + "crypto/tls"
23 + "crypto/x509"
24 + "crypto/x509/pkix"
25 + "math/big"
26 + "testing"
27 + "time"
28 +
29 + "github.com/caddyserver/certmagic"
30 + "go.uber.org/zap"
31 +)
32 +
33 +func benchCert(tb testing.TB) tls.Certificate {
34 + tb.Helper()
35 + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
36 + if err != nil {
37 + tb.Fatal(err)
38 + }
39 + tmpl := &x509.Certificate{
40 + SerialNumber: big.NewInt(1),
41 + Subject: pkix.Name{CommonName: "example.com"},
42 + DNSNames: []string{"example.com"},
43 + NotBefore: time.Now().Add(-time.Hour),
44 + NotAfter: time.Now().Add(24 * time.Hour),
45 + }
46 + der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &key.PublicKey, key)
47 + if err != nil {
48 + tb.Fatal(err)
49 + }
50 + leaf, err := x509.ParseCertificate(der)
51 + if err != nil {
52 + tb.Fatal(err)
53 + }
54 + return tls.Certificate{Certificate: [][]byte{der}, PrivateKey: key, Leaf: leaf}
55 +}
56 +
57 +// benchmarkCertLookup measures CertMagic's GetCertificate, which Caddy calls
58 +// once per TLS handshake. CertMagic emits "tls_get_certificate" there, so this
59 +// shows what the events app costs a handshake when nothing is subscribed.
60 +func benchmarkCertLookup(b *testing.B, onEvent func(context.Context, string, map[string]any) error) {
61 + b.Helper()
62 +
63 + var cfg *certmagic.Config
64 + cache := certmagic.NewCache(certmagic.CacheOptions{
65 + GetConfigForCert: func(certmagic.Certificate) (*certmagic.Config, error) { return cfg, nil },
66 + Logger: zap.NewNop(),
67 + })
68 + b.Cleanup(cache.Stop)
69 +
70 + cfg = certmagic.New(cache, certmagic.Config{
71 + Storage: &certmagic.FileStorage{Path: b.TempDir()},
72 + Logger: zap.NewNop(),
73 + OnEvent: onEvent,
74 + })
75 + if _, err := cfg.CacheUnmanagedTLSCertificate(context.Background(), benchCert(b), nil); err != nil {
76 + b.Fatal(err)
77 + }
78 +
79 + hello := &tls.ClientHelloInfo{
80 + ServerName: "example.com",
81 + CipherSuites: []uint16{tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256},
82 + SupportedCurves: []tls.CurveID{tls.CurveP256},
83 + SupportedPoints: []uint8{0},
84 + SupportedVersions: []uint16{tls.VersionTLS13, tls.VersionTLS12},
85 + SignatureSchemes: []tls.SignatureScheme{tls.ECDSAWithP256AndSHA256},
86 + }
87 + if _, err := cfg.GetCertificate(hello); err != nil {
88 + b.Fatal(err)
89 + }
90 +
91 + b.ReportAllocs()
92 + b.ResetTimer()
93 + for i := 0; i < b.N; i++ {
94 + if _, err := cfg.GetCertificate(hello); err != nil {
95 + b.Fatal(err)
96 + }
97 + }
98 +}
99 +
100 +// Caddy always binds the TLS app to the events app, so this is what a
101 +// handshake pays for the tls_get_certificate event in a config that has no
102 +// subscriptions at all -- which is every config that does not use the events
103 +// app. The hook body is what (*caddytls.TLS).onEvent does.
104 +func BenchmarkCertLookupWithEventsApp(b *testing.B) {
105 + app, ctx, cancel := testApp(b)
106 + defer cancel()
107 +
108 + benchmarkCertLookup(b, func(_ context.Context, name string, data map[string]any) error {
109 + return app.Emit(ctx, name, data).Aborted
110 + })
111 +}