@marcel91200 / caddy Mirror · read-only / commits / 355c1782
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, 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