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, 12:15 UTC
chore: Use atomics where appropriate (#7648)
* chore: Use atomics where appropriate * Use atomic for shutdownAt
Francis Lavoie committed
Apr 25, 2026 at 03:47 UTC
355c1782137f678897495503dcfe0b9e77997737
8 files changed
+47
-51
caddy.go
+3
-3
@@ -766,7 +766,7 @@ func Validate(cfg *Config) error {
766
// code is emitted.
767
func exitProcess(ctx context.Context, logger *zap.Logger) {
768
// let the rest of the program know we're quitting; only do it once
769
- if !atomic.CompareAndSwapInt32(exiting, 0, 1) {
769
+ if !exiting.CompareAndSwap(false, true) {
770
return
771
}
772
@@ -845,11 +845,11 @@ func exitProcess(ctx context.Context, logger *zap.Logger) {
845
}()
846
}
847
848
-var exiting = new(int32) // accessed atomically
848
+var exiting atomic.Bool
849
850
// Exiting returns true if the process is exiting.
851
// EXPERIMENTAL API: subject to change or removal.
852
-func Exiting() bool { return atomic.LoadInt32(exiting) == 1 }
852
+func Exiting() bool { return exiting.Load() }
853
854
// OnExit registers a callback to invoke during process exit.
855
// This registration is PROCESS-GLOBAL, meaning that each
listen.go
+10
-10
@@ -120,8 +120,8 @@ func listenReusable(ctx context.Context, lnKey string, network, address string,
120
// re-wrapped in a new fakeCloseListener each time the listener
121
// is reused. This type is atomic and values must not be copied.
122
type fakeCloseListener struct {
123
- closed int32 // accessed atomically; belongs to this struct only
124
- *sharedListener // embedded, so we also become a net.Listener
123
+ closed atomic.Bool
124
+ *sharedListener // embedded, so we also become a net.Listener
125
keepAliveConfig net.KeepAliveConfig
126
}
127
@@ -131,7 +131,7 @@ type canSetKeepAliveConfig interface {
131
132
func (fcl *fakeCloseListener) Accept() (net.Conn, error) {
133
// if the listener is already "closed", return error
134
- if atomic.LoadInt32(&fcl.closed) == 1 {
134
+ if fcl.closed.Load() {
135
return nil, fakeClosedErr(fcl)
136
}
137
@@ -155,7 +155,7 @@ func (fcl *fakeCloseListener) Accept() (net.Conn, error) {
155
// that we set when Close() was called, and return a non-temporary and
156
// non-timeout error value to the caller, masking the "true" error, so
157
// that server loops / goroutines won't retry, linger, and leak
158
- if atomic.LoadInt32(&fcl.closed) == 1 {
158
+ if fcl.closed.Load() {
159
// we dereference the sharedListener explicitly even though it's embedded
160
// so that it's clear in the code that side-effects are shared with other
161
// users of this listener, not just our own reference to it; we also don't
@@ -175,7 +175,7 @@ func (fcl *fakeCloseListener) Accept() (net.Conn, error) {
175
// underlying listener. The underlying listener is only closed
176
// if the caller is the last known user of the socket.
177
func (fcl *fakeCloseListener) Close() error {
178
- if atomic.CompareAndSwapInt32(&fcl.closed, 0, 1) {
178
+ if fcl.closed.CompareAndSwap(false, true) {
179
// There are two ways I know of to get an Accept()
180
// function to return to the server loop that called
181
// it: close the listener, or set a deadline in the
@@ -238,13 +238,13 @@ func (sl *sharedListener) Destruct() error {
238
// fakeClosePacketConn is like fakeCloseListener, but for PacketConns,
239
// or more specifically, *net.UDPConn
240
type fakeClosePacketConn struct {
241
- closed int32 // accessed atomically; belongs to this struct only
242
- *sharedPacketConn // embedded, so we also become a net.PacketConn; its key is used in Close
241
+ closed atomic.Bool
242
+ *sharedPacketConn // embedded, so we also become a net.PacketConn; its key is used in Close
243
}
244
245
func (fcpc *fakeClosePacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
246
// if the listener is already "closed", return error
247
- if atomic.LoadInt32(&fcpc.closed) == 1 {
247
+ if fcpc.closed.Load() {
248
return 0, nil, &net.OpError{
249
Op: "readfrom",
250
Net: fcpc.LocalAddr().Network(),
@@ -258,7 +258,7 @@ func (fcpc *fakeClosePacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err e
258
if err != nil {
259
// this server was stopped, so clear the deadline and let
260
// any new server continue reading; but we will exit
261
- if atomic.LoadInt32(&fcpc.closed) == 1 {
261
+ if fcpc.closed.Load() {
262
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
263
if err = fcpc.SetReadDeadline(time.Time{}); err != nil {
264
return n, addr, err
@@ -273,7 +273,7 @@ func (fcpc *fakeClosePacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err e
273
274
// Close won't close the underlying socket unless there is no more reference, then listenerPool will close it.
275
func (fcpc *fakeClosePacketConn) Close() error {
276
- if atomic.CompareAndSwapInt32(&fcpc.closed, 0, 1) {
276
+ if fcpc.closed.CompareAndSwap(false, true) {
277
_ = fcpc.SetReadDeadline(time.Now()) // unblock ReadFrom() calls to kick old servers out of their loops
278
_, _ = listenerPool.Delete(fcpc.sharedPacketConn.key)
279
}
listen_unix.go
+12
-10
@@ -63,7 +63,7 @@ func reuseUnixSocket(network, addr string) (any, error) {
63
if err != nil {
64
return nil, err
65
}
66
- atomic.AddInt32(unixSocket.count, 1)
66
+ unixSocket.count.Add(1)
67
unixSockets[socketKey] = &unixListener{ln.(*net.UnixListener), socketKey, unixSocket.count}
68
69
case *unixConn:
@@ -71,7 +71,7 @@ func reuseUnixSocket(network, addr string) (any, error) {
71
if err != nil {
72
return nil, err
73
}
74
- atomic.AddInt32(unixSocket.count, 1)
74
+ unixSocket.count.Add(1)
75
unixSockets[socketKey] = &unixConn{pc.(*net.UnixConn), socketKey, unixSocket.count}
76
}
77
@@ -165,8 +165,9 @@ func listenReusable(ctx context.Context, lnKey string, network, address string,
165
if !fd {
166
// TODO: Not 100% sure this is necessary, but we do this for net.UnixListener, so...
167
if unix, ok := ln.(*net.UnixConn); ok {
168
- one := int32(1)
169
- ln = &unixConn{unix, lnKey, &one}
168
+ cnt := new(atomic.Int32)
169
+ cnt.Store(1)
170
+ ln = &unixConn{unix, lnKey, cnt}
171
unixSockets[lnKey] = ln.(*unixConn)
172
}
173
}
@@ -181,8 +182,9 @@ func listenReusable(ctx context.Context, lnKey string, network, address string,
182
// (we do our own "unlink on close" -- not required, but more tidy)
183
if unix, ok := ln.(*net.UnixListener); ok {
184
unix.SetUnlinkOnClose(false)
184
- one := int32(1)
185
- ln = &unixListener{unix, lnKey, &one}
185
+ cnt := new(atomic.Int32)
186
+ cnt.Store(1)
187
+ ln = &unixListener{unix, lnKey, cnt}
188
unixSockets[lnKey] = ln.(*unixListener)
189
}
190
}
@@ -216,11 +218,11 @@ func reusePort(network, address string, conn syscall.RawConn) error {
218
type unixListener struct {
219
*net.UnixListener
220
mapKey string
219
- count *int32 // accessed atomically
221
+ count *atomic.Int32
222
}
223
224
func (uln *unixListener) Close() error {
223
- newCount := atomic.AddInt32(uln.count, -1)
225
+ newCount := uln.count.Add(-1)
226
if newCount == 0 {
227
file, err := uln.File()
228
var name string
@@ -242,11 +244,11 @@ func (uln *unixListener) Close() error {
244
type unixConn struct {
245
*net.UnixConn
246
mapKey string
245
- count *int32 // accessed atomically
247
+ count *atomic.Int32
248
}
249
250
func (uc *unixConn) Close() error {
249
- newCount := atomic.AddInt32(uc.count, -1)
251
+ newCount := uc.count.Add(-1)
252
if newCount == 0 {
253
file, err := uc.File()
254
var name string
listeners.go
+5
-5
@@ -624,8 +624,8 @@ func fakeClosedErr(l interface{ Addr() net.Addr }) error {
624
var errFakeClosed = fmt.Errorf("QUIC listener 'closed' 😉")
625
626
type fakeCloseQuicListener struct {
627
- closed int32 // accessed atomically; belongs to this struct only
628
- *sharedQuicListener // embedded, so we also become a quic.EarlyListener
627
+ closed atomic.Int32
628
+ *sharedQuicListener // embedded, so we also become a quic.EarlyListener
629
context context.Context
630
contextCancel context.CancelCauseFunc
631
}
@@ -642,16 +642,16 @@ func (fcql *fakeCloseQuicListener) Accept(_ context.Context) (*quic.Conn, error)
642
}
643
644
// if the listener is "closed", return a fake closed error instead
645
- if atomic.LoadInt32(&fcql.closed) == 1 && errors.Is(err, context.Canceled) {
645
+ if fcql.closed.Load() == 1 && errors.Is(err, context.Canceled) {
646
return nil, fakeClosedErr(fcql)
647
}
648
return nil, err
649
}
650
651
func (fcql *fakeCloseQuicListener) Close() error {
652
- if atomic.CompareAndSwapInt32(&fcql.closed, 0, 1) {
652
+ if fcql.closed.CompareAndSwap(0, 1) {
653
fcql.contextCancel(errFakeClosed)
654
- } else if atomic.CompareAndSwapInt32(&fcql.closed, 1, 2) {
654
+ } else if fcql.closed.CompareAndSwap(1, 2) {
655
_, _ = listenerPool.Delete(fcql.sharedQuicListener.key)
656
}
657
return nil
modules/caddyhttp/app.go
+1
-5
@@ -219,8 +219,6 @@ func (app *App) Provision(ctx caddy.Context) error {
219
srv.ctx = ctx
220
srv.logger = app.logger.Named("log")
221
srv.errorLogger = app.logger.Named("log.error")
222
- srv.shutdownAtMu = new(sync.RWMutex)
223
-
222
if srv.Metrics != nil {
223
srv.logger.Warn("per-server 'metrics' is deprecated; use 'metrics' in the root 'http' app instead")
224
app.Metrics = cmp.Or(app.Metrics, &Metrics{
@@ -694,9 +692,7 @@ func (app *App) Stop() error {
692
for _, addr := range na.Expand() {
693
if caddy.ListenerUsage(addr.Network, addr.JoinHostPort(0)) < 2 {
694
app.logger.Debug("listener closing and shutdown delay is configured", zap.String("address", addr.String()))
697
- server.shutdownAtMu.Lock()
698
- server.shutdownAt = scheduledTime
699
- server.shutdownAtMu.Unlock()
695
+ server.shutdownAt.Store(&scheduledTime)
696
delay = true
697
} else {
698
app.logger.Debug("shutdown delay configured but listener will remain open", zap.String("address", addr.String()))
modules/caddyhttp/replacer.go
+4
-7
@@ -387,17 +387,14 @@ func addHTTPVarsToReplacer(repl *caddy.Replacer, req *http.Request, w http.Respo
387
switch key {
388
case "http.shutting_down":
389
server := req.Context().Value(ServerCtxKey).(*Server)
390
- server.shutdownAtMu.RLock()
391
- defer server.shutdownAtMu.RUnlock()
392
- return !server.shutdownAt.IsZero(), true
390
+ return server.shutdownAt.Load() != nil, true
391
case "http.time_until_shutdown":
392
server := req.Context().Value(ServerCtxKey).(*Server)
395
- server.shutdownAtMu.RLock()
396
- defer server.shutdownAtMu.RUnlock()
397
- if server.shutdownAt.IsZero() {
393
+ t := server.shutdownAt.Load()
394
+ if t == nil {
395
return nil, true
396
}
400
- return time.Until(server.shutdownAt), true
397
+ return time.Until(*t), true
398
}
399
400
return nil, false
modules/caddyhttp/server.go
+2
-3
@@ -28,7 +28,7 @@ import (
28
"runtime"
29
"slices"
30
"strings"
31
- "sync"
31
+ "sync/atomic"
32
"time"
33
34
"github.com/caddyserver/certmagic"
@@ -291,8 +291,7 @@ type Server struct {
291
292
trustedProxies IPRangeSource
293
294
- shutdownAt time.Time
295
- shutdownAtMu *sync.RWMutex
294
+ shutdownAt atomic.Pointer[time.Time]
295
296
// registered callback functions
297
connStateFuncs []func(net.Conn, http.ConnState)
usagepool.go
+10
-8
@@ -79,14 +79,15 @@ func (up *UsagePool) LoadOrNew(key any, construct Constructor) (value any, loade
79
up.Lock()
80
upv, loaded = up.pool[key]
81
if loaded {
82
- atomic.AddInt32(&upv.refs, 1)
82
+ upv.refs.Add(1)
83
up.Unlock()
84
upv.RLock()
85
value = upv.value
86
err = upv.err
87
upv.RUnlock()
88
} else {
89
- upv = &usagePoolVal{refs: 1}
89
+ upv = &usagePoolVal{}
90
+ upv.refs.Store(1)
91
upv.Lock()
92
up.pool[key] = upv
93
up.Unlock()
@@ -118,7 +119,7 @@ func (up *UsagePool) LoadOrStore(key, val any) (value any, loaded bool) {
119
up.Lock()
120
upv, loaded = up.pool[key]
121
if loaded {
121
- atomic.AddInt32(&upv.refs, 1)
122
+ upv.refs.Add(1)
123
up.Unlock()
124
upv.Lock()
125
if upv.err == nil {
@@ -129,7 +130,8 @@ func (up *UsagePool) LoadOrStore(key, val any) (value any, loaded bool) {
130
}
131
upv.Unlock()
132
} else {
132
- upv = &usagePoolVal{refs: 1, value: val}
133
+ upv = &usagePoolVal{value: val}
134
+ upv.refs.Store(1)
135
up.pool[key] = upv
136
up.Unlock()
137
value = val
@@ -173,7 +175,7 @@ func (up *UsagePool) Delete(key any) (deleted bool, err error) {
175
up.Unlock()
176
return false, nil
177
}
176
- refs := atomic.AddInt32(&upv.refs, -1)
178
+ refs := upv.refs.Add(-1)
179
if refs == 0 {
180
delete(up.pool, key)
181
up.Unlock()
@@ -188,7 +190,7 @@ func (up *UsagePool) Delete(key any) (deleted bool, err error) {
190
up.Unlock()
191
if refs < 0 {
192
panic(fmt.Sprintf("deleted more than stored: %#v (usage: %d)",
191
- upv.value, upv.refs))
193
+ upv.value, upv.refs.Load()))
194
}
195
}
196
return deleted, err
@@ -203,7 +205,7 @@ func (up *UsagePool) References(key any) (int, bool) {
205
if loaded {
206
// I wonder if it'd be safer to read this value during
207
// our lock on the UsagePool... guess we'll see...
206
- refs := atomic.LoadInt32(&upv.refs)
208
+ refs := upv.refs.Load()
209
return int(refs), true
210
}
211
return 0, false
@@ -220,7 +222,7 @@ type Destructor interface {
222
}
223
224
type usagePoolVal struct {
223
- refs int32 // accessed atomically; must be 64-bit aligned for 32-bit systems
225
+ refs atomic.Int32
226
value any
227
err error
228
sync.RWMutex