Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS
Mirrored from https://github.com/Slazee/caddy.git
Synced Nov 14, 05:15 UTC
logging: Implement dial timeout for net writer (fix #4083) (#4172)
* logging: Implement dial timeout for net writer (fix #4083) * Limit how often redials are attempted This should cause dial blocking to occur only once every 10 seconds at most, but it also means the logger connection might be down for up to 10 seconds after it comes back online; oh well. We shouldn't block for DialTimeout at every single log emission. * Clarify offline behavior
Matt Holt committed
May 19, 2021 at 15:14 UTC
e3c369d4526e44f23efb10aaad8a60ce519720a0
1 file changed
+61
-14
modules/logging/netwriter.go
+61
-14
@@ -18,7 +18,9 @@ import (
18
"fmt"
19
"io"
20
"net"
21
+ "os"
22
"sync"
23
+ "time"
24
25
"github.com/caddyserver/caddy/v2"
26
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
@@ -28,10 +30,16 @@ func init() {
30
caddy.RegisterModule(NetWriter{})
31
}
32
31
-// NetWriter implements a log writer that outputs to a network socket.
33
+// NetWriter implements a log writer that outputs to a network socket. If
34
+// the socket goes down, it will dump logs to stderr while it attempts to
35
+// reconnect.
36
type NetWriter struct {
37
+ // The address of the network socket to which to connect.
38
Address string `json:"address,omitempty"`
39
40
+ // The timeout to wait while connecting to the socket.
41
+ DialTimeout caddy.Duration `json:"dial_timeout,omitempty"`
42
+
43
addr caddy.NetworkAddress
44
}
45
@@ -60,6 +68,10 @@ func (nw *NetWriter) Provision(ctx caddy.Context) error {
68
return fmt.Errorf("multiple ports not supported")
69
}
70
71
+ if nw.DialTimeout < 0 {
72
+ return fmt.Errorf("timeout cannot be less than 0")
73
+ }
74
+
75
return nil
76
}
77
@@ -74,7 +86,10 @@ func (nw NetWriter) WriterKey() string {
86
87
// OpenWriter opens a new network connection.
88
func (nw NetWriter) OpenWriter() (io.WriteCloser, error) {
77
- reconn := &redialerConn{nw: nw}
89
+ reconn := &redialerConn{
90
+ nw: nw,
91
+ timeout: time.Duration(nw.DialTimeout),
92
+ }
93
conn, err := reconn.dial()
94
if err != nil {
95
return nil, err
@@ -87,7 +102,9 @@ func (nw NetWriter) OpenWriter() (io.WriteCloser, error) {
102
103
// UnmarshalCaddyfile sets up the handler from Caddyfile tokens. Syntax:
104
//
90
-// net <address>
105
+// net <address> {
106
+// dial_timeout <duration>
107
+// }
108
//
109
func (nw *NetWriter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
110
for d.Next() {
@@ -98,6 +115,22 @@ func (nw *NetWriter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
115
if d.NextArg() {
116
return d.ArgErr()
117
}
118
+ for nesting := d.Nesting(); d.NextBlock(nesting); {
119
+ switch d.Val() {
120
+ case "dial_timeout":
121
+ if !d.NextArg() {
122
+ return d.ArgErr()
123
+ }
124
+ timeout, err := caddy.ParseDuration(d.Val())
125
+ if err != nil {
126
+ return d.Errf("invalid duration: %s", d.Val())
127
+ }
128
+ if d.NextArg() {
129
+ return d.ArgErr()
130
+ }
131
+ nw.DialTimeout = caddy.Duration(timeout)
132
+ }
133
+ }
134
}
135
return nil
136
}
@@ -107,8 +140,10 @@ func (nw *NetWriter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
140
// is retried.
141
type redialerConn struct {
142
net.Conn
110
- connMu sync.RWMutex
111
- nw NetWriter
143
+ connMu sync.RWMutex
144
+ nw NetWriter
145
+ timeout time.Duration
146
+ lastRedial time.Time
147
}
148
149
// Write wraps the underlying Conn.Write method, but if that fails,
@@ -131,20 +166,32 @@ func (reconn *redialerConn) Write(b []byte) (n int, err error) {
166
return
167
}
168
134
- // we're the lucky first goroutine to re-dial the connection
135
- conn2, err2 := reconn.dial()
136
- if err2 != nil {
137
- return
138
- }
139
- if n, err = conn2.Write(b); err == nil {
140
- reconn.Conn.Close()
141
- reconn.Conn = conn2
169
+ // there's still a problem, so try to re-attempt dialing the socket
170
+ // if some time has passed in which the issue could have potentially
171
+ // been resolved - we don't want to block at every single log
172
+ // emission (!) - see discussion in #4111
173
+ if time.Since(reconn.lastRedial) > 10*time.Second {
174
+ reconn.lastRedial = time.Now()
175
+ conn2, err2 := reconn.dial()
176
+ if err2 != nil {
177
+ // logger socket still offline; instead of discarding the log, dump it to stderr
178
+ os.Stderr.Write(b)
179
+ return
180
+ }
181
+ if n, err = conn2.Write(b); err == nil {
182
+ reconn.Conn.Close()
183
+ reconn.Conn = conn2
184
+ }
185
+ } else {
186
+ // last redial attempt was too recent; just dump to stderr for now
187
+ os.Stderr.Write(b)
188
}
189
+
190
return
191
}
192
193
func (reconn *redialerConn) dial() (net.Conn, error) {
147
- return net.Dial(reconn.nw.addr.Network, reconn.nw.addr.JoinHostPort(0))
194
+ return net.DialTimeout(reconn.nw.addr.Network, reconn.nw.addr.JoinHostPort(0), reconn.timeout)
195
}
196
197
// Interface guards