@@ -23,12 +23,15 @@ import (
23
23
"strings"
24
24
"sync"
25
25
"sync/atomic"
26
+ "time"
26
27
27
28
"github.com/digitalocean/go-openvswitch/ovsdb/internal/jsonrpc"
28
29
)
29
30
30
- // A Client is an OVSDB client.
31
+ // A Client is an OVSDB client. Clients can be customized by using OptionFuncs
32
+ // in the Dial and New functions.
31
33
type Client struct {
34
+ // The RPC connection, and its logger.
32
35
c * jsonrpc.Conn
33
36
ll * log.Logger
34
37
@@ -39,7 +42,14 @@ type Client struct {
39
42
cbMu sync.RWMutex
40
43
callbacks map [int ]callback
41
44
42
- wg * sync.WaitGroup
45
+ // Interval at which echo RPCs should occur in the background, and statistics
46
+ // about the echo loop.
47
+ echoInterval time.Duration
48
+ echoOK , echoFail * int64
49
+
50
+ // Track and clean up background goroutines.
51
+ cancel func ()
52
+ wg * sync.WaitGroup
43
53
}
44
54
45
55
// An OptionFunc is a function which can configure a Client.
@@ -53,6 +63,18 @@ func Debug(ll *log.Logger) OptionFunc {
53
63
}
54
64
}
55
65
66
+ // EchoInterval specifies an interval at which the Client will send
67
+ // echo RPCs to an OVSDB server to keep the connection alive. If this
68
+ // option is not used, the Client will not send any echo RPCs on its own.
69
+ //
70
+ // Specify a duration of 0 to disable sending background echo RPCs.
71
+ func EchoInterval (d time.Duration ) OptionFunc {
72
+ return func (c * Client ) error {
73
+ c .echoInterval = d
74
+ return nil
75
+ }
76
+ }
77
+
56
78
// Dial dials a connection to an OVSDB server and returns a Client.
57
79
func Dial (network , addr string , options ... OptionFunc ) (* Client , error ) {
58
80
conn , err := net .Dial (network , addr )
@@ -82,10 +104,26 @@ func New(conn net.Conn, options ...OptionFunc) (*Client, error) {
82
104
// Set up callbacks.
83
105
client .callbacks = make (map [int ]callback )
84
106
85
- // Start up any background routines.
107
+ // Start up any background routines, and enable canceling them via context.
108
+ ctx , cancel := context .WithCancel (context .Background ())
109
+ client .cancel = cancel
110
+
86
111
var wg sync.WaitGroup
87
112
wg .Add (1 )
88
113
114
+ // If configured, send echo RPCs in the background at a fixed interval.
115
+ var echoOK , echoFail int64
116
+ client .echoOK = & echoOK
117
+ client .echoFail = & echoFail
118
+
119
+ if d := client .echoInterval ; d != 0 {
120
+ wg .Add (1 )
121
+ go func () {
122
+ defer wg .Done ()
123
+ client .echoLoop (ctx , d )
124
+ }()
125
+ }
126
+
89
127
// Handle all incoming RPC responses and notifications.
90
128
go func () {
91
129
defer wg .Done ()
@@ -102,8 +140,9 @@ func (c *Client) requestID() int {
102
140
return int (atomic .AddInt64 (c .rpcID , 1 ))
103
141
}
104
142
105
- // Close closes a Client's connection.
143
+ // Close closes a Client's connection and cleans up its resources .
106
144
func (c * Client ) Close () error {
145
+ c .cancel ()
107
146
err := c .c .Close ()
108
147
c .wg .Wait ()
109
148
return err
@@ -114,9 +153,11 @@ func (c *Client) Stats() ClientStats {
114
153
var s ClientStats
115
154
116
155
c .cbMu .RLock ()
117
- defer c .cbMu .RUnlock ()
118
-
119
156
s .Callbacks .Current = len (c .callbacks )
157
+ c .cbMu .RUnlock ()
158
+
159
+ s .EchoLoop .Success = int (atomic .LoadInt64 (c .echoOK ))
160
+ s .EchoLoop .Failure = int (atomic .LoadInt64 (c .echoFail ))
120
161
121
162
return s
122
163
}
@@ -129,6 +170,13 @@ type ClientStats struct {
129
170
// for RPC responses.
130
171
Current int
131
172
}
173
+
174
+ // Statistics about the Client's internal echo RPC loop.
175
+ // Note that all counters will be zero if the echo loop is disabled.
176
+ EchoLoop struct {
177
+ // The number of successful and failed echo RPCs in the loop.
178
+ Success , Failure int
179
+ }
132
180
}
133
181
134
182
// rpc performs a single RPC request, and checks the response for errors.
@@ -204,7 +252,7 @@ func (c *Client) listen() {
204
252
res , err := c .c .Receive ()
205
253
if err != nil {
206
254
// EOF or closed connection means time to stop serving.
207
- if err == io .EOF || strings . Contains (err . Error (), "use of closed network" ) {
255
+ if err == io .EOF || isClosedNetwork (err ) {
208
256
return
209
257
}
210
258
@@ -229,6 +277,43 @@ func (c *Client) listen() {
229
277
}
230
278
}
231
279
280
+ // echoLoop starts a loop that sends echo RPCs at the interval defined by d.
281
+ func (c * Client ) echoLoop (ctx context.Context , d time.Duration ) {
282
+ t := time .NewTicker (d )
283
+ defer t .Stop ()
284
+
285
+ for {
286
+ // If context is canceled, we should exit this loop. If a tick is fired
287
+ // and the context was already canceled, we exit there as well.
288
+ select {
289
+ case <- ctx .Done ():
290
+ return
291
+ case <- t .C :
292
+ if err := ctx .Err (); err != nil {
293
+ return
294
+ }
295
+ }
296
+
297
+ // For the time being, we will track metrics about the number of successes
298
+ // and failures while sending echo RPCs.
299
+ // TODO(mdlayher): improve error handling for echo loop.
300
+ if err := c .Echo (ctx ); err != nil {
301
+ if isClosedNetwork (err ) {
302
+ // Our socket was closed, which means the context should be canceled
303
+ // and we should terminate on the next loop. No need to increment
304
+ // errors counter.
305
+ continue
306
+ }
307
+
308
+ // Count other errors as failures.
309
+ atomic .AddInt64 (c .echoFail , 1 )
310
+ continue
311
+ }
312
+
313
+ atomic .AddInt64 (c .echoOK , 1 )
314
+ }
315
+ }
316
+
232
317
// A callback can be used to send a message back to a caller, or
233
318
// allow the caller to cancel waiting for a message.
234
319
type callback struct {
@@ -275,6 +360,16 @@ func (c *Client) doCallback(id int, res rpcResponse) {
275
360
delete (c .callbacks , id )
276
361
}
277
362
363
+ // isClosedNetwork checks for errors caused by a closed network connection.
364
+ func isClosedNetwork (err error ) bool {
365
+ if err == nil {
366
+ return false
367
+ }
368
+
369
+ // Not an awesome solution, but see: https://github.com/golang/go/issues/4373.
370
+ return strings .Contains (err .Error (), "use of closed network connection" )
371
+ }
372
+
278
373
func panicf (format string , a ... interface {}) {
279
374
panic (fmt .Sprintf (format , a ... ))
280
375
}
0 commit comments