I have a RabbitMQ consumer script in Go. This is a simple script from RabbitMQ tutorial that uses streadway/amqp library.
The problem is that if the RabbitMQ server is stopped, the consumer script does not exit; and when RabbitMQ server is restarted, the consumer does not receive messages anymore.
Is there a way to detect that the consumer connection is dead and reconnect, or at least terminate the consumer script?
I know that the library sets a default 10 sec. heartbeat interval for the connection; is it possible to use that someway?
func main() {
conn, err := amqp.Dial("amqp://guest:guest#localhost:5672/")
failOnError(err, "Failed to connect to RabbitMQ")
defer conn.Close()
ch, err := conn.Channel()
failOnError(err, "Failed to open a channel")
defer ch.Close()
q, err := ch.QueueDeclare(
"test_task_queue", // name
true, // durable
false, // delete when unused
false, // exclusive
false, // no-wait
nil, // arguments
)
failOnError(err, "Failed to declare a queue")
err = ch.Qos(
1, // prefetch count
0, // prefetch size
false, // global
)
failOnError(err, "Failed to set QoS")
msgs, err := ch.Consume(
q.Name, // queue
"", // consumer
false, // auto-ack
false, // exclusive
false, // no-local
false, // no-wait
nil, // args
)
failOnError(err, "Failed to register a consumer")
forever := make(chan bool)
go func() {
for d := range msgs {
log.Printf("Received a message: %s", d.Body)
d.Ack(false)
dot_count := bytes.Count(d.Body, []byte("."))
t := time.Duration(dot_count)
time.Sleep(t * time.Second)
log.Printf("Done")
}
}()
log.Printf(" [*] Waiting for messages. To exit press CTRL+C")
<-forever
}
amqp.Connection has method NotifyClose() which return channel signalling a transport or protocol error.
So something like
for { //reconnection loop
conn, err := amqp.Dial("amqp://guest:guest#localhost:5672/") //setup
notify := conn.NotifyClose(make(chan *amqp.Error)) //error channel
...
ch, err := conn.Channel()
msgs, err := ch.Consume(
...
for{ //receive loop
select { //check connection
case err = <-notify:
//work with error
break //reconnect
case d = <- msgs:
//work with message
...
}
}
}
There are a couple ways of doing this: checking whether the delivery channel is closed or using Channel.NotifyClose.
Checking the delivery channel
After starting the consumer, you will receive from the delivery channel. As you know, the receive operation may take the special form x, ok := <-ch, where ok is false when x has a zero value due the channel being closed (and empty):
conn, _ := amqp.Dial(url)
ch, _ := conn.Channel()
delivery, _ := ch.Consume(
queueName,
consumerName,
true, // auto ack
false, // exclusive
false, // no local
true, // no wait,
nil, // table
)
for {
payload, ok := <- delivery
if !ok {
// ... channel closed
return
}
}
This works because the Go channel <-chan amqp.Delivery will be closed when the AMQP channel is closed or an error occurs:
[It] continues deliveries to the returned chan Delivery until Channel.Cancel, Connection.Close, Channel.Close, or an AMQP exception occurs.
Using Channel.NotifyClose
This is straightforward. And the principle is the same:
NotifyClose registers a listener for when the server sends a channel or connection exception in the form of a Connection.Close or Channel.Close method.
The channel returned by NotifyClose is the same you pass as argument; the method only registers it internally, so you can do just:
errC := ch.NotifyClose(make(chan *amqp.Error, n))
where n is a non-zero buffer size. Make sure to pass a buffered channel to NotifyClose otherwise, depending on how your code is structured, the library may block on send.
Then you can receive on the errC channel and take action depending on the type of error you get. In short, the error can be:
a connection error, usually unrecoverable
a channel error, also called soft exception, usually recoverable by resetting the connection
nil if the program calls conn.Close() on purpose
To know whether the error is recoverable or not, you can inspect the amqp.Error's Code field and/or the Recover field, which is set to true in case of soft exceptions.
The following func shows how error codes can be distinguished — this is provided as additional insight. For the general case, just check Error.Recover:
const (
ConnectionError = 1
ChannelError = 2
)
func isConnectionError(err *amqp.Error) bool {
return errorType(err.Code) == ConnectionError
}
func isChannelError(err *amqp.Error) bool {
return errorType(err.Code) == ChannelError
}
func errorType(code int) int {
switch code {
case
amqp.ContentTooLarge, // 311
amqp.NoConsumers, // 313
amqp.AccessRefused, // 403
amqp.NotFound, // 404
amqp.ResourceLocked, // 405
amqp.PreconditionFailed: // 406
return ChannelError
case
amqp.ConnectionForced, // 320
amqp.InvalidPath, // 402
amqp.FrameError, // 501
amqp.SyntaxError, // 502
amqp.CommandInvalid, // 503
amqp.ChannelError, // 504
amqp.UnexpectedFrame, // 505
amqp.ResourceError, // 506
amqp.NotAllowed, // 530
amqp.NotImplemented, // 540
amqp.InternalError: // 541
fallthrough
default:
return ConnectionError
}
}
This may help someone
// MAIN PACKAGE - "cmd/my-project-name/main.go"
package main
import (
"my-proyect-name/rmq"
)
func main() {
// RMQ
rmq.ConnectToRMQ()
}
// RMQ PACKAGE - "rmq"
import (
"errors"
"log"
"ms-gcp-cloud-storage/constants"
"time"
amqp "github.com/rabbitmq/amqp091-go"
)
const (
rmqCredentials string = "amqp://user:pswd#localhost:5672"
rmqQueue string = "golang-queue:new"
rmqExchange string = constants.RMQ_DIRECT_EXCHANGE // "" empty string
rmqContentType string = "application/json"
)
var conn *amqp.Connection
var chann *amqp.Channel
func hasError(err error, msg string) {
if err != nil {
log.Printf("%s: %s", msg, err)
}
}
func ConnectToRMQ() (err error) {
conn, err = amqp.Dial(rmqCredentials)
if err != nil {
return errors.New("Error de conexion: " + err.Error())
}
chann, err = conn.Channel()
if err != nil {
return errors.New("Error al abrir canal: " + err.Error())
}
q, err := chann.QueueDeclare(
rmqQueue, // name
true, // durable
false, // delete when unused
false, // exclusive
false, // no-wait
nil, // arguments
)
if err != nil {
log.Fatalf("Error al declarar queue %v\n", q.Name)
}
log.Printf("Conectado al Queue: %v\n", q.Name)
observeConnection()
return nil
}
func observeConnection() {
go func() {
log.Printf("Conexion perdida: %s\n", <-conn.NotifyClose(make(chan *amqp.Error)))
log.Printf("Intentando reconectar con RMQ\n")
closeActiveConnections()
for err := ConnectToRMQ(); err != nil; err = ConnectToRMQ() {
log.Println(err)
time.Sleep(5 * time.Second)
}
}()
}
// Can be also implemented in graceful shutdowns
func closeActiveConnections() {
if !chann.IsClosed() {
if err := chann.Close(); err != nil {
log.Println(err.Error())
}
}
if conn != nil && !conn.IsClosed() {
if err := conn.Close(); err != nil {
log.Println(err.Error())
}
}
}
// SendMessage - message without response
func SendMessage(body string) {
err := chann.Publish(
rmqExchange, // exchange
rmqQueue, // routing key
false, // mandatory
false, // immediate
amqp.Publishing{
ContentType: rmqContentType,
DeliveryMode: constants.RMQ_PERSISTENT_MSG,
Body: []byte(body),
})
if err != nil {
log.Printf("%s\n %s\n", "Error al publicar mensaje", err)
log.Println(body)
}
}
It has not been found that the go-amqp library implements the disconnection and reconnection function of the connection pool.
There is an open source code based on Amqp secondary packaging on github.
Reconnect after disconnection and abnormal reconnect have been supported. The code is also relatively simple to use, and each service has a connection and channel.
Source Code here
Example Code:
package main
import (
"go-rabbit/rabbit"
)
/*
support isconnection and reconnection function
And Failure re-send function
#author : Bill
*/
func main() {
var(
addr = "amqp://guest:guest#localhost:5672/"
queue = "testQueue"
exchange = "test_exchange"
routerKey = "/test"
msg = "test1!"
//delay
delayQueue = "delay_queue"
delayExchange = "delay_exchange"
delayRouterKey = "delay_exchange"
prefix = "v1_prefix"
sep = "_"
eType = "F"
_ttl = 60 * 1000
)
var rabbitProduct1 = rabbit.NewRabbitProduct(addr,_ttl,prefix,sep,delayExchange,delayQueue,delayRouterKey)
// register recycle
go rabbitProduct1.InitDefdelay(false)
go rabbitProduct1.InitDefdelay(true)
go rabbitProduct1.RegisterDelayWithPreFix("delay_queue","delay_exchange","delay_exchange")
// ttl is dead recycle time if ttl > 0 then recycle
rabbitProduct1.PubMessage(true,eType,queue,exchange,routerKey,msg,rabbitProduct1.GetBool(1),rabbitProduct1.GetBool(0),_ttl)
}
Wish it will help you or give you some idea
Related
I got code that consumes messages from RabbitMQ queue. It should fail if it doesn`t received message for some time.
msgs, err := ch.Consume(
c.ResponseQueue, // queue
"", // consumer
false, // auto-ack
false, // exclusive
false, // no-local
false, // no-wait
nil, // args
)
failOnError(err, "Failed to register a consumer")
...
loop:
for timeout := time.After(time.Second); ; {
select {
case <-timeout:
log.Printf("Failed to receive response for action %+v\n Payload: %+v\nError: %+v\n", action, body, err)
return errors.New("Failed to receive response for action")
default:
for d := range msgs {
if corrID == d.CorrelationId {
err = json.Unmarshal([]byte(uncompress(d.Body)), &v)
if err != nil {
return err
}
ch.Ack(d.DeliveryTag, false)
break loop
}
}
}
}
I took consume code from RabbitMQ manual and tried some advices for implementing timeout. I know how to do it in Java, but can`t repeat it in Golang.
Thanks in advance.
Update:
Changed select to this:
c1 := make(chan error, 1)
go func() {
for d := range msgs {
if corrID == d.CorrelationId {
err = json.Unmarshal([]byte(uncompress(d.Body)), &v)
if err != nil {
c1 <- err
}
ch.Ack(d.DeliveryTag, false)
c1 <- nil
}
}
}()
select {
case <-time.After(defaultTimeout * time.Second):
log.Printf("Failed to receive response for action %+v\n Payload: %+v\nError: %+v\n", action, body, err)
return errors.New("Failed to receive response in time for action")
case err := <-c1:
failOnError(err, "Failed to process response")
}
return err
Now it works as expected - if it doesn`t receive message with proper corellationId it will fail with timeout. Thanks for help everyone.
Your loop has a select with 2 cases: a timeout and a default branch. Upon entering the loop the timeout will not fire, so the default branch is executed.
The default branch contains a for range over the msgs channel which keeps receiving from the channel until it is closed (and all values have been received from it). Normally this shouldn't happen, so the timeout case will not be revisited (only if some error occurs and msgs is closed).
Instead inside the loop use a select with 2 cases, one timeout and one that receives only a single value from msgs. If a message is received, restart the timeout. For a restartable timer use time.Timer.
timeout := time.Second
timer := time.NewTimer(timeout)
for {
select {
case <-timer.C:
fmt.Println("timeout, returning")
return
case msg := <-msgs:
fmt.Println("received message:", msg)
// Reset timer: it must be stopped first
// (and drain its channel if it reports false)
if !timer.Stop() {
<-timer.C
}
timer.Reset(timeout)
}
}
Check this Go Playground example to see it in action.
Note that if you don't need to reset the timer once a message is received, just comment out the resetter code. Also, if no reset is needed, time.After() is simpler:
timeout := time.After(time.Second)
for {
select {
case <-timeout:
fmt.Println("timeout, returning")
return
case msg := <-msgs:
fmt.Println("received message:", msg, time.Now())
}
}
Try this one on the Go Playground.
One final note: if you would break from the loop before the timeout happens, the timer in the background would not be freed immediately (only when the timeout happens). If you need this operation frequently, you may use context.WithTimeout() to obtain a context.Context and a cancel function which you may call immediately before returning to free up the timer resource (preferably as deferred).
This is how it would look like:
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
for {
select {
case <-ctx.Done():
fmt.Println("timeout, returning")
return
case msg := <-msgs:
fmt.Println("received message:", msg, time.Now())
}
}
Try this one on the Go Playground.
Changed select to this:
c1 := make(chan error, 1)
go func() {
for d := range msgs {
if corrID == d.CorrelationId {
err = json.Unmarshal([]byte(uncompress(d.Body)), &v)
if err != nil {
c1 <- err
}
ch.Ack(d.DeliveryTag, false)
c1 <- nil
}
}
}()
select {
case <-time.After(defaultTimeout * time.Second):
log.Printf("Failed to receive response for action %+v\n Payload: %+v\nError: %+v\n", action, body, err)
return errors.New("Failed to receive response in time for action")
case err := <-c1:
failOnError(err, "Failed to process response")
}
return err
For reproducing an error (404) Reason: "NOT_FOUND - no queue when subscribing to rabbitmq queues, I am using the following code to concurrently declare and consume queues:
package main
import (
"fmt"
"log"
"os"
"sync"
"time"
uuid "github.com/satori/go.uuid"
"github.com/streadway/amqp"
)
func exit1(err error, msg string) {
if err != nil {
log.Fatalf("%s: %s", msg, err)
}
}
func main() {
rUSER := "bunny"
rPASS := "test"
rHOST := "my-rabbit"
rPORT := "5672"
rVHOST := "hole"
// read from ENV
if e := os.Getenv("RABBITMQ_USER"); e != "" {
rUSER = e
}
if e := os.Getenv("RABBITMQ_PASS"); e != "" {
rPASS = e
}
if e := os.Getenv("RABBITMQ_HOST"); e != "" {
rHOST = e
}
if e := os.Getenv("RABBITMQ_PORT"); e != "" {
rPORT = e
}
if e := os.Getenv("RABBITMQ_VHOST"); e != "" {
rVHOST = e
}
conn, err := amqp.Dial(fmt.Sprintf("amqp://%s:%s#%s:%s/%s",
rUSER, rPASS, rHOST, rPORT, rVHOST))
exit1(err, "Failed to connect to RabbitMQ")
defer conn.Close()
ch, err := conn.Channel()
exit1(err, "Failed to open a channel")
defer ch.Close()
// buggy part
args := map[string]interface{}{
"x-message-ttl": int32(3000),
"x-expires": int32(8000), // <-- culprit
}
concurrent := 500
wg := sync.WaitGroup{}
semaphore := make(chan struct{}, concurrent)
for i := 0; i < 1000; i++ {
semaphore <- struct{}{}
wg.Add(1)
go func() {
queueName := fmt.Sprintf("carrot-%s-%s", time.Now().Format("2006-01-02"), uuid.Must(uuid.NewV4()))
fmt.Printf("Creating queue: %s\n", queueName)
defer func() {
<-semaphore
wg.Done()
}()
q, err := ch.QueueDeclare(
queueName,
false, // durable
false, // delete when usused
false, // exclusive
false, // no-wait
args, // arguments
)
exit1(err, "Failed to declare a queue")
// how to measure here time elapsed between ch.Consume is called
_, err = ch.Consume(
q.Name, // queue
"", // consumer
true, // auto-ack
false, // exclusive
false, // no-local
false, // no-wait
nil, // args
)
exit1(err, "Failed to register a consumer")
}()
}
wg.Wait()
}
Giving more context, the code will work fine and subscribe to all
the 1000 queues when having few concurrent clients < 100, but when adding more concurrent clients a kind of "race condition" occurs and clients start to receive an error 404, this happens because of declaring a TTL for a queue, in this case, 8 seconds:
"x-expires": int32(8000),
A better solution would be to use exclusive queues otherwise, the queue is being deleted before the client can consume it, but within this "buggy" code, I would like to measure the delay is taking between the ch.QueueDeclare and the ch.Consume.
A client basically is doing:
q, err := ch.QueueDeclare(
queueName,
false, // durable
false, // delete when usused
false, // exclusive
false, // no-wait
args, // arguments
)
And then:
_, err = ch.Consume(
q.Name, // queue
"", // consumer
true, // auto-ack
false, // exclusive
false, // no-local
false, // no-wait
nil, // args
)
When having multiple concurrent clients, after doing the QueueDeclare there seems to be a delay that reaches up to 8s seconds before the Consume is called and because of this the error 404 but what and how could I instrument, adapt to code to measure this delays?
I'm attempting to read URLs from a queue (RabbitMQ) and make a limited number of concurrent HTTP requests i.e. have a pool of 10 workers making concurrent requests to URLs received from the queue (forever).
So far I've implemented a consumer as per the RabbitMQ tutorials:
https://www.rabbitmq.com/tutorials/tutorial-one-go.html
And have tried a number of methods from examples discovered on the web, ending at the example here:
http://jmoiron.net/blog/limiting-concurrency-in-go/
Unfortunately, my current code runs for approximately one minute and then freezes indefinitely. I've tried adding/moving go routines around but I can't seem to get it to work as intended (I'm very new to Go).
Current code:
package main
import (
"fmt"
"log"
"net/http"
"time"
"github.com/Xide/bloom"
"github.com/streadway/amqp"
)
func failOnError(err error, msg string) {
if err != nil {
log.Fatalf("%s: %s", msg, err)
panic(fmt.Sprintf("%s: %s", msg, err))
}
}
var netClient = &http.Client{
Timeout: time.Second * 10,
}
func getRequest(url string) {
//resp, err := http.Get(string(url))
resp, err := netClient.Get(string(url))
if err != nil {
log.Printf("HTTP request error: %s", err)
return
}
fmt.Println("StatusCode:", resp.StatusCode)
fmt.Println(resp.Request.URL)
}
func main() {
bf := bloom.NewDefaultScalable(0.1)
conn, err := amqp.Dial("amqp://127.0.0.1:5672/")
failOnError(err, "Failed to connect to RabbitMQ")
defer conn.Close()
ch, err := conn.Channel()
failOnError(err, "Failed to open a channel")
defer ch.Close()
q, err := ch.QueueDeclare(
"urls", // name
true, // durable
false, // delete when unused
false, // exclusive
false, // no-wait
nil, // arguments
)
failOnError(err, "Failed to declare a queue")
err = ch.Qos(
1, // prefetch count
0, // prefetch size
false, //global
)
failOnError(err, "Failed to set Qos")
msgs, err := ch.Consume(
q.Name, // queue
"", // consumer
false, // auto-ack
false, // exclusive
false, // no-local
false, // no-wait
nil, // args
)
failOnError(err, "Failed to register a consumer")
forever := make(chan bool)
concurrency := 10
sem := make(chan bool, concurrency)
go func() {
for d := range msgs {
sem <- true
url := string(d.Body)
if bf.Match(url) == false {
bf.Feed(url)
log.Printf("Not seen: %s", d.Body)
go func(url string) {
defer func() { <-sem }()
getRequest(url)
}(url)
} else {
log.Printf("Already seen: %s", d.Body)
}
d.Ack(false)
}
for i := 0; i < cap(sem); i++ {
sem <- true
}
}()
log.Printf(" [*] Waiting for messages. To exit press CTRL+C")
<-forever
}
You're not properly handling your HTTP responses, leading to a growing set of open connections. Try this:
func getRequest(url string) {
resp, err := netClient.Get(string(url))
if err != nil {
log.Printf("HTTP request error: %s", err)
return
}
// Add this bit:
defer func() {
io.Copy(ioutil.Discard, resp.Body)
resp.Body.Close()
}()
fmt.Println("StatusCode:", resp.StatusCode)
fmt.Println(resp.Request.URL)
}
This, after you finish reading messages from the channel, seems unnecessary and potentially problematic:
for i := 0; i < cap(sem); i++ {
sem <- true
}
Why fill the sem channel after you've read all the messages from the queue? You've added exactly as many messages to the channel as you expect to read from it, so this is pointless at best, and could cause problems if you make the wrong change to the rest of the code.
Unrelated to your issue, but this is redundant:
if err != nil {
log.Fatalf("%s: %s", msg, err)
panic(fmt.Sprintf("%s: %s", msg, err))
}
Per the documentation, Fatalf already exits, so the panic will never be called. If you want to log and panic, try log.Panicf, which is designed for that purpose.
You are adding to sem when you get a message, but only removing from sem when you haven't seen a url.
so, once you've "already seen" 10 urls, your app will lock up.
So you need to add <-sem to your else statement that logs "Already seen".
Either way, that's a fairly odd way to do this kind of concurrency.
Here's a version that does this in a more idiomatic way on Play.
Note, in this version, we just spawn 10 goroutines that listen to the rabbit channel.
package main
import (
"fmt"
"log"
"net/http"
"time"
"github.com/Xide/bloom"
"github.com/streadway/amqp"
)
func failOnError(err error, msg string) {
if err != nil {
log.Fatalf("%s: %s", msg, err)
}
}
var netClient = &http.Client{
Timeout: time.Second * 10,
}
func getRequest(url string) {
//resp, err := http.Get(string(url))
resp, err := netClient.Get(string(url))
if err != nil {
log.Printf("HTTP request error: %s", err)
return
}
resp.Body.Close()
fmt.Println("StatusCode:", resp.StatusCode)
fmt.Println(resp.Request.URL)
}
func main() {
bf := bloom.NewDefaultScalable(0.1)
conn, err := amqp.Dial("amqp://127.0.0.1:5672/")
failOnError(err, "Failed to connect to RabbitMQ")
defer conn.Close()
ch, err := conn.Channel()
failOnError(err, "Failed to open a channel")
defer ch.Close()
q, err := ch.QueueDeclare(
"urls", // name
true, // durable
false, // delete when unused
false, // exclusive
false, // no-wait
nil, // arguments
)
failOnError(err, "Failed to declare a queue")
err = ch.Qos(
1, // prefetch count
0, // prefetch size
false, //global
)
failOnError(err, "Failed to set Qos")
msgs, err := ch.Consume(
q.Name, // queue
"", // consumer
false, // auto-ack
false, // exclusive
false, // no-local
false, // no-wait
nil, // args
)
failOnError(err, "Failed to register a consumer")
concurrency := 10
var wg sync.Waitgroup // used to coordinate when they are done, ie: if rabbit conn was closed
for x := 0; x < concurrency; x++ { // spawn 10 goroutines, all reading from the rabbit channel
wg.Add(1)
go func() {
defer wg.Done() // signal that this goroutine is done
for d := range msgs {
url := string(d.Body)
if bf.Match(url) == false {
bf.Feed(url)
log.Printf("Not seen: %s", d.Body)
getRequest(url)
} else {
log.Printf("Already seen: %s", d.Body)
}
d.Ack(false)
}
log.Println("msgs channel closed")
}()
}
log.Printf(" [*] Waiting for messages. To exit press CTRL+C")
wg.Wait() // when all goroutine's exit, the app exits
}
I am writing a websocket service in golang.
The program use gollira websocket to accept ws request, and in each request handler, it listen to rabbitmq queue for messages.
The problem is, when i close browser window, the handler thread is still running, i guess there is an mechanism to be notified when connection disconnected.
I try to listen to channel request.Context().Done(), when i doesn't work.
package main
import (
"fmt"
"log"
"net/http"
"github.com/gorilla/websocket"
"github.com/streadway/amqp"
)
var (
addr = "localhost:9999"
upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool { return true },
}
)
var conn *amqp.Connection
func watch(w http.ResponseWriter, r *http.Request) {
ns := r.URL.Query().Get("ns")
if ns == "" {
return
}
c, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Print("upgrade:", err)
return
}
defer c.Close()
ch, err := conn.Channel()
failOnError(err, "Failed to open a channel")
defer ch.Close()
err = ch.ExchangeDeclare(
"notify", // name
"fanout", // type
true, // durable
false, // auto-deleted
false, // internal
false, // no-wait
nil, // arguments
)
failOnError(err, "Failed to declare an exchange")
q, err := ch.QueueDeclare(
"", // name
false, // durable
false, // delete when usused
true, // exclusive
false, // no-wait
nil, // arguments
)
failOnError(err, "Failed to declare a queue")
err = ch.QueueBind(
q.Name, // queue name
ns, // routing key
"dsm_tasks_notify", // exchange
false,
nil)
failOnError(err, "Failed to bind a queue")
msgs, err := ch.Consume(
q.Name, // queue
"", // consumer
true, // auto-ack
false, // exclusive
false, // no-local
false, // no-wait
nil, // args
)
failOnError(err, "Failed to register a consumer")
for {
select {
case d := <-msgs:
err = c.WriteMessage(websocket.TextMessage, d.Body)
if err != nil {
log.Println("write:", err)
break
}
case <-r.Context().Done():
log.Println("Disconnect")
return
}
}
}
func failOnError(err error, msg string) {
if err != nil {
log.Fatalf("%s: %s", msg, err)
panic(fmt.Sprintf("%s: %s", msg, err))
}
}
func main() {
var err error
conn, err = amqp.Dial("amqp://guest:guest#localhost:5672/")
failOnError(err, "Failed to connect to RabbitMQ")
defer conn.Close()
http.HandleFunc("/watch", watch)
log.Fatal(http.ListenAndServe(addr, nil))
}
If the browser cleanly closes the connection, then read on the webssocket connection returns an error. Cleanup the websocket connection as you would on any read error.
The application must PING the connection and expect the corresponding PONGs to detect other situations. The chat example shows how to send PINGs and receive PONGs.
I have a RabbitMQ consumer script in Go. This is a simple script from RabbitMQ tutorial that uses streadway/amqp library.
The problem is that if the RabbitMQ server is stopped, the consumer script does not exit; and when RabbitMQ server is restarted, the consumer does not receive messages anymore.
Is there a way to detect that the consumer connection is dead and reconnect, or at least terminate the consumer script?
I know that the library sets a default 10 sec. heartbeat interval for the connection; is it possible to use that someway?
func main() {
conn, err := amqp.Dial("amqp://guest:guest#localhost:5672/")
failOnError(err, "Failed to connect to RabbitMQ")
defer conn.Close()
ch, err := conn.Channel()
failOnError(err, "Failed to open a channel")
defer ch.Close()
q, err := ch.QueueDeclare(
"test_task_queue", // name
true, // durable
false, // delete when unused
false, // exclusive
false, // no-wait
nil, // arguments
)
failOnError(err, "Failed to declare a queue")
err = ch.Qos(
1, // prefetch count
0, // prefetch size
false, // global
)
failOnError(err, "Failed to set QoS")
msgs, err := ch.Consume(
q.Name, // queue
"", // consumer
false, // auto-ack
false, // exclusive
false, // no-local
false, // no-wait
nil, // args
)
failOnError(err, "Failed to register a consumer")
forever := make(chan bool)
go func() {
for d := range msgs {
log.Printf("Received a message: %s", d.Body)
d.Ack(false)
dot_count := bytes.Count(d.Body, []byte("."))
t := time.Duration(dot_count)
time.Sleep(t * time.Second)
log.Printf("Done")
}
}()
log.Printf(" [*] Waiting for messages. To exit press CTRL+C")
<-forever
}
amqp.Connection has method NotifyClose() which return channel signalling a transport or protocol error.
So something like
for { //reconnection loop
conn, err := amqp.Dial("amqp://guest:guest#localhost:5672/") //setup
notify := conn.NotifyClose(make(chan *amqp.Error)) //error channel
...
ch, err := conn.Channel()
msgs, err := ch.Consume(
...
for{ //receive loop
select { //check connection
case err = <-notify:
//work with error
break //reconnect
case d = <- msgs:
//work with message
...
}
}
}
There are a couple ways of doing this: checking whether the delivery channel is closed or using Channel.NotifyClose.
Checking the delivery channel
After starting the consumer, you will receive from the delivery channel. As you know, the receive operation may take the special form x, ok := <-ch, where ok is false when x has a zero value due the channel being closed (and empty):
conn, _ := amqp.Dial(url)
ch, _ := conn.Channel()
delivery, _ := ch.Consume(
queueName,
consumerName,
true, // auto ack
false, // exclusive
false, // no local
true, // no wait,
nil, // table
)
for {
payload, ok := <- delivery
if !ok {
// ... channel closed
return
}
}
This works because the Go channel <-chan amqp.Delivery will be closed when the AMQP channel is closed or an error occurs:
[It] continues deliveries to the returned chan Delivery until Channel.Cancel, Connection.Close, Channel.Close, or an AMQP exception occurs.
Using Channel.NotifyClose
This is straightforward. And the principle is the same:
NotifyClose registers a listener for when the server sends a channel or connection exception in the form of a Connection.Close or Channel.Close method.
The channel returned by NotifyClose is the same you pass as argument; the method only registers it internally, so you can do just:
errC := ch.NotifyClose(make(chan *amqp.Error, n))
where n is a non-zero buffer size. Make sure to pass a buffered channel to NotifyClose otherwise, depending on how your code is structured, the library may block on send.
Then you can receive on the errC channel and take action depending on the type of error you get. In short, the error can be:
a connection error, usually unrecoverable
a channel error, also called soft exception, usually recoverable by resetting the connection
nil if the program calls conn.Close() on purpose
To know whether the error is recoverable or not, you can inspect the amqp.Error's Code field and/or the Recover field, which is set to true in case of soft exceptions.
The following func shows how error codes can be distinguished — this is provided as additional insight. For the general case, just check Error.Recover:
const (
ConnectionError = 1
ChannelError = 2
)
func isConnectionError(err *amqp.Error) bool {
return errorType(err.Code) == ConnectionError
}
func isChannelError(err *amqp.Error) bool {
return errorType(err.Code) == ChannelError
}
func errorType(code int) int {
switch code {
case
amqp.ContentTooLarge, // 311
amqp.NoConsumers, // 313
amqp.AccessRefused, // 403
amqp.NotFound, // 404
amqp.ResourceLocked, // 405
amqp.PreconditionFailed: // 406
return ChannelError
case
amqp.ConnectionForced, // 320
amqp.InvalidPath, // 402
amqp.FrameError, // 501
amqp.SyntaxError, // 502
amqp.CommandInvalid, // 503
amqp.ChannelError, // 504
amqp.UnexpectedFrame, // 505
amqp.ResourceError, // 506
amqp.NotAllowed, // 530
amqp.NotImplemented, // 540
amqp.InternalError: // 541
fallthrough
default:
return ConnectionError
}
}
This may help someone
// MAIN PACKAGE - "cmd/my-project-name/main.go"
package main
import (
"my-proyect-name/rmq"
)
func main() {
// RMQ
rmq.ConnectToRMQ()
}
// RMQ PACKAGE - "rmq"
import (
"errors"
"log"
"ms-gcp-cloud-storage/constants"
"time"
amqp "github.com/rabbitmq/amqp091-go"
)
const (
rmqCredentials string = "amqp://user:pswd#localhost:5672"
rmqQueue string = "golang-queue:new"
rmqExchange string = constants.RMQ_DIRECT_EXCHANGE // "" empty string
rmqContentType string = "application/json"
)
var conn *amqp.Connection
var chann *amqp.Channel
func hasError(err error, msg string) {
if err != nil {
log.Printf("%s: %s", msg, err)
}
}
func ConnectToRMQ() (err error) {
conn, err = amqp.Dial(rmqCredentials)
if err != nil {
return errors.New("Error de conexion: " + err.Error())
}
chann, err = conn.Channel()
if err != nil {
return errors.New("Error al abrir canal: " + err.Error())
}
q, err := chann.QueueDeclare(
rmqQueue, // name
true, // durable
false, // delete when unused
false, // exclusive
false, // no-wait
nil, // arguments
)
if err != nil {
log.Fatalf("Error al declarar queue %v\n", q.Name)
}
log.Printf("Conectado al Queue: %v\n", q.Name)
observeConnection()
return nil
}
func observeConnection() {
go func() {
log.Printf("Conexion perdida: %s\n", <-conn.NotifyClose(make(chan *amqp.Error)))
log.Printf("Intentando reconectar con RMQ\n")
closeActiveConnections()
for err := ConnectToRMQ(); err != nil; err = ConnectToRMQ() {
log.Println(err)
time.Sleep(5 * time.Second)
}
}()
}
// Can be also implemented in graceful shutdowns
func closeActiveConnections() {
if !chann.IsClosed() {
if err := chann.Close(); err != nil {
log.Println(err.Error())
}
}
if conn != nil && !conn.IsClosed() {
if err := conn.Close(); err != nil {
log.Println(err.Error())
}
}
}
// SendMessage - message without response
func SendMessage(body string) {
err := chann.Publish(
rmqExchange, // exchange
rmqQueue, // routing key
false, // mandatory
false, // immediate
amqp.Publishing{
ContentType: rmqContentType,
DeliveryMode: constants.RMQ_PERSISTENT_MSG,
Body: []byte(body),
})
if err != nil {
log.Printf("%s\n %s\n", "Error al publicar mensaje", err)
log.Println(body)
}
}
It has not been found that the go-amqp library implements the disconnection and reconnection function of the connection pool.
There is an open source code based on Amqp secondary packaging on github.
Reconnect after disconnection and abnormal reconnect have been supported. The code is also relatively simple to use, and each service has a connection and channel.
Source Code here
Example Code:
package main
import (
"go-rabbit/rabbit"
)
/*
support isconnection and reconnection function
And Failure re-send function
#author : Bill
*/
func main() {
var(
addr = "amqp://guest:guest#localhost:5672/"
queue = "testQueue"
exchange = "test_exchange"
routerKey = "/test"
msg = "test1!"
//delay
delayQueue = "delay_queue"
delayExchange = "delay_exchange"
delayRouterKey = "delay_exchange"
prefix = "v1_prefix"
sep = "_"
eType = "F"
_ttl = 60 * 1000
)
var rabbitProduct1 = rabbit.NewRabbitProduct(addr,_ttl,prefix,sep,delayExchange,delayQueue,delayRouterKey)
// register recycle
go rabbitProduct1.InitDefdelay(false)
go rabbitProduct1.InitDefdelay(true)
go rabbitProduct1.RegisterDelayWithPreFix("delay_queue","delay_exchange","delay_exchange")
// ttl is dead recycle time if ttl > 0 then recycle
rabbitProduct1.PubMessage(true,eType,queue,exchange,routerKey,msg,rabbitProduct1.GetBool(1),rabbitProduct1.GetBool(0),_ttl)
}
Wish it will help you or give you some idea