Skip to content

Commit a0f116d

Browse files
authored
Merge pull request #109 from 0x4b53/restart-server-ch
Add support to simpler restart server by using a restart channel
2 parents a5ac1c1 + 91a1768 commit a0f116d

File tree

4 files changed

+119
-15
lines changed

4 files changed

+119
-15
lines changed

client.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -395,7 +395,8 @@ func (c *Client) runOnce() error {
395395

396396
go c.runPublisher(outputCh)
397397

398-
err = monitorAndWait(
398+
_, err = monitorAndWait(
399+
make(chan struct{}),
399400
c.stopChan,
400401
inputConn.NotifyClose(make(chan *amqp.Error)),
401402
outputConn.NotifyClose(make(chan *amqp.Error)),

connection.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ type PublishSettings struct {
103103
ConfirmMode bool
104104
}
105105

106-
func monitorAndWait(stopChan chan struct{}, amqpErrs ...chan *amqp.Error) error {
106+
func monitorAndWait(restartChan, stopChan chan struct{}, amqpErrs ...chan *amqp.Error) (bool, error) {
107107
result := make(chan error, len(amqpErrs))
108108

109109
// Setup monitoring for connections and channels, can be several connections and several channels.
@@ -121,9 +121,11 @@ func monitorAndWait(stopChan chan struct{}, amqpErrs ...chan *amqp.Error) error
121121

122122
select {
123123
case err := <-result:
124-
return err
124+
return true, err
125+
case <-restartChan:
126+
return true, nil
125127
case <-stopChan:
126-
return nil
128+
return false, nil
127129
}
128130
}
129131

server.go

Lines changed: 65 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,10 @@ type Server struct {
6969
// channel will be closed when Stop() is called.
7070
stopChan chan struct{}
7171

72+
// restartChan channel is used to signal restarts. It can be set by the user
73+
// so they can restart the server without having to call Stop()/Start()
74+
restartChan chan struct{}
75+
7276
// isRunning is 1 when the server is running.
7377
isRunning int32
7478

@@ -94,6 +98,9 @@ func NewServer(url string) *Server {
9498
errorLog: log.Printf, // use the standard logger default.
9599
//nolint:revive // Keep variables for clarity
96100
debugLog: func(format string, args ...interface{}) {}, // don't print anything default.
101+
// We ensure to always create a channel so we can call `Restart` without
102+
// blocking.
103+
restartChan: make(chan struct{}),
97104
}
98105

99106
server.setDefaults()
@@ -190,6 +197,14 @@ func (s *Server) WithDebugLogger(f LogFunc) *Server {
190197
return s
191198
}
192199

200+
// WithRestartChan will add a channel to the server that will trigger a restart
201+
// when it's triggered.
202+
func (s *Server) WithRestartChan(ch chan struct{}) *Server {
203+
s.restartChan = ch
204+
205+
return s
206+
}
207+
193208
// AddMiddleware will add a ServerMiddleware to the list of middlewares to be
194209
// triggered before the handle func for each request.
195210
func (s *Server) AddMiddleware(m ServerMiddlewareFunc) *Server {
@@ -241,7 +256,7 @@ func (s *Server) ListenAndServe() {
241256
}
242257

243258
for {
244-
err := s.listenAndServe()
259+
shouldRestart, err := s.listenAndServe()
245260
// If we couldn't run listenAndServe and an error was returned, make
246261
// sure to check if the stopChan was closed - a user might know about
247262
// connection problems and have call Stop(). If the channel isn't
@@ -259,6 +274,17 @@ func (s *Server) ListenAndServe() {
259274
}
260275
}
261276

277+
if shouldRestart {
278+
// We must set up responses again. It's required to close to shut
279+
// down the responders so we let the shutdown process close it and
280+
// then we re-create it here.
281+
s.responses = make(chan processedRequest)
282+
283+
s.debugLog("server: listener restarting")
284+
285+
continue
286+
}
287+
262288
s.debugLog("server: listener exiting gracefully")
263289

264290
break
@@ -267,7 +293,7 @@ func (s *Server) ListenAndServe() {
267293
atomic.StoreInt32(&s.isRunning, 0)
268294
}
269295

270-
func (s *Server) listenAndServe() error {
296+
func (s *Server) listenAndServe() (bool, error) {
271297
s.debugLog("server: starting listener: %s", s.url)
272298

273299
// We are using two different connections here because:
@@ -277,15 +303,15 @@ func (s *Server) listenAndServe() error {
277303
// -- https://godoc.org/github.com/rabbitmq/amqp091-go#Channel.Consume
278304
inputConn, outputConn, err := createConnections(s.url, s.dialconfig)
279305
if err != nil {
280-
return err
306+
return false, err
281307
}
282308

283309
defer inputConn.Close()
284310
defer outputConn.Close()
285311

286312
inputCh, outputCh, err := createChannels(inputConn, outputConn)
287313
if err != nil {
288-
return err
314+
return false, err
289315
}
290316

291317
defer inputCh.Close()
@@ -297,7 +323,7 @@ func (s *Server) listenAndServe() error {
297323
false,
298324
)
299325
if err != nil {
300-
return err
326+
return false, err
301327
}
302328

303329
// Notify everyone that the server has started.
@@ -312,7 +338,7 @@ func (s *Server) listenAndServe() error {
312338
// cancel our consumers.
313339
consumerTags, err := s.startConsumers(inputCh, &consumersWg)
314340
if err != nil {
315-
return err
341+
return false, err
316342
}
317343

318344
// This WaitGroup will reach 0 when the responder() has finished sending
@@ -322,23 +348,28 @@ func (s *Server) listenAndServe() error {
322348

323349
go s.responder(outputCh, &responderWg)
324350

325-
err = monitorAndWait(
351+
shouldRestart, err := monitorAndWait(
352+
s.restartChan,
326353
s.stopChan,
327354
inputConn.NotifyClose(make(chan *amqp.Error)),
328355
outputConn.NotifyClose(make(chan *amqp.Error)),
329356
inputCh.NotifyClose(make(chan *amqp.Error)),
330357
outputCh.NotifyClose(make(chan *amqp.Error)),
331358
)
332359
if err != nil {
333-
return err
360+
return shouldRestart, err
334361
}
335362

336-
s.debugLog("server: gracefully shutting down")
363+
if shouldRestart {
364+
s.debugLog("server: restarting server")
365+
} else {
366+
s.debugLog("server: gracefully shutting down")
367+
}
337368

338369
// 1. Tell amqp we want to shut down by canceling all the consumers.
339370
err = cancelConsumers(inputCh, consumerTags)
340371
if err != nil {
341-
return err
372+
return shouldRestart, err
342373
}
343374

344375
// 3. We've told amqp to stop delivering messages, now we wait for all
@@ -355,7 +386,7 @@ func (s *Server) listenAndServe() error {
355386
// 5. We have no more messages incoming and we've published all our
356387
// responses. The closing of connections and channels are deferred so we can
357388
// just return now.
358-
return nil
389+
return shouldRestart, nil
359390
}
360391

361392
func (s *Server) startConsumers(inputCh *amqp.Channel, wg *sync.WaitGroup) ([]string, error) {
@@ -499,6 +530,29 @@ func (s *Server) Stop() {
499530
close(s.stopChan)
500531
}
501532

533+
// Restart will gracefully disconnect from AMQP exactly like `Stop` but instead
534+
// of returning from `ListenAndServe` it will set everything up again from
535+
// scratch and start listening again. This can be useful if a server restart is
536+
// wanted without running `ListenAndServe` in a loop.
537+
func (s *Server) Restart() {
538+
// Restart is noop if not running.
539+
if atomic.LoadInt32(&s.isRunning) == 0 {
540+
return
541+
}
542+
543+
// Ensure we never block on the restartChan, if we're in the middle of a
544+
// setup or teardown process we won't be listening on this channel and if so
545+
// we do a noop.
546+
// This can likely happen e.g. if you have multiple messages in memory and
547+
// acknowledging them stops working, you might call `Restart` on all of them
548+
// but only the first one should trigger the restart.
549+
select {
550+
case s.restartChan <- struct{}{}:
551+
default:
552+
s.debugLog("server: no listener on restartChan, ensure server is running")
553+
}
554+
}
555+
502556
// cancelConsumers will cancel the specified consumers.
503557
func cancelConsumers(channel *amqp.Channel, consumerTags []string) error {
504558
for _, consumerTag := range consumerTags {

server_test.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package amqprpc
33
import (
44
"context"
55
"fmt"
6+
"log"
67
"testing"
78
"time"
89

@@ -151,6 +152,52 @@ func TestServerReconnect(t *testing.T) {
151152
assert.Equal(t, []byte("Hello"), reply.Body)
152153
}
153154

155+
func TestManualRestart(t *testing.T) {
156+
hasStarted := make(chan struct{})
157+
restartChan := make(chan struct{})
158+
159+
s := NewServer(testURL).
160+
WithRestartChan(restartChan).
161+
WithDebugLogger(log.Printf).
162+
WithAutoAck(false)
163+
164+
s.OnStarted(func(_, _ *amqp.Connection, _, _ *amqp.Channel) {
165+
hasStarted <- struct{}{}
166+
})
167+
168+
s.Bind(DirectBinding("myqueue", func(_ context.Context, rw *ResponseWriter, d amqp.Delivery) {
169+
_ = d.Ack(false)
170+
171+
fmt.Fprintf(rw, "Hello")
172+
}))
173+
174+
// Wait for the initial startup signal.
175+
go func() { <-hasStarted }()
176+
177+
stop := startAndWait(s)
178+
defer stop()
179+
180+
c := NewClient(testURL)
181+
defer c.Stop()
182+
183+
request := NewRequest().WithRoutingKey("myqueue")
184+
reply, err := c.Send(request)
185+
require.NoError(t, err)
186+
assert.Equal(t, []byte("Hello"), reply.Body)
187+
188+
// We only care about one restart but let's call multiple ones to ensure
189+
// we're not blocking.
190+
s.Restart()
191+
s.Restart()
192+
s.Restart()
193+
<-hasStarted
194+
195+
request = NewRequest().WithRoutingKey("myqueue")
196+
reply, err = c.Send(request)
197+
require.NoError(t, err)
198+
assert.Equal(t, []byte("Hello"), reply.Body)
199+
}
200+
154201
func TestServerOnStarted(t *testing.T) {
155202
errs := make(chan string, 4)
156203

0 commit comments

Comments
 (0)