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
Related
I have an application (let's call it client) connecting to another process (let's call it server) on the same machine via gRPC. The communication goes over unix socket.
If server is restarted, my client gets an EOF and does not re-establish the connection, although I expected the clientConn to handle the reconnection automatically.
Why isn't the dialer taking care of the reconnection?
I expect it to do so with the backoff params I passed.
Below some pseudo-MWE.
Run establish the initial connection, then spawns goroutineOne
goroutineOne waits for the connection to be ready and delegates the send to fooUpdater
fooUpdater streams the data, or returns in case of errors
for waitUntilReady I used the pseudo-code referenced by this answer to get a new stream.
func main() {
go func() {
if err := Run(ctx); err != nil {
log.Errorf("connection error: %v", err)
}
ctxCancel()
}()
// some wait logic
}
func Run(ctx context.Context) {
backoffConfig := backoff.Config{
BaseDelay: time.Duration(1 * time.Second),
Multiplier: backoff.DefaultConfig.Multiplier,
Jitter: backoff.DefaultConfig.Jitter,
MaxDelay: time.Duration(120 * time.Second),
}
myConn, err := grpc.DialContext(ctx,
"/var/run/foo.bar",
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithConnectParams(grpc.ConnectParams{Backoff: backoffConfig, MinConnectTimeout: time.Duration(1 * time.Second)}),
grpc.WithContextDialer(func(ctx context.Context, addr string) (net.Conn, error) {
d := net.Dialer{}
c, err := d.DialContext(ctx, "unix", addr)
if err != nil {
return nil, fmt.Errorf("connection to unix://%s failed: %w", addr, err)
}
return c, nil
}),
)
if err != nil {
return fmt.Errorf("could not establish socket for foo: %w", err)
}
defer myConn.Close()
return goroutineOne()
}
func goroutineOne() {
reconnect := make(chan struct{})
for {
if ready := waitUntilReady(ctx, myConn, time.Duration(2*time.Minute)); !ready {
return fmt.Errorf("myConn: %w, timeout: %s", ErrWaitReadyTimeout, "2m")
}
go func() {
if err := fooUpdater(ctx, dataBuffer, myConn); err != nil {
log.Errorf("foo updater: %v", err)
}
reconnect <- struct{}{}
}()
select {
case <-ctx.Done():
return nil
case <-reconnect:
}
}
}
func fooUpdater(ctx context.Context, dataBuffer custom.CircularBuffer, myConn *grpc.ClientConn) error {
clientStream, err := myConn.Stream(ctx) // custom pb code, returns grpc.ClientConn.NewStream(...)
if err != nil {
return fmt.Errorf("could not obtain stream: %w", err)
}
for {
select {
case <-ctx.Done():
return nil
case data := <-dataBuffer:
if err := clientStream.Send(data); err != nil {
return fmt.Errorf("could not send data: %w", err)
}
}
}
}
func waitUntilReady(ctx context.Context, conn *grpc.ClientConn, maxTimeout time.Duration) bool {
ctx, cancel := context.WithTimeout(ctx, maxTimeout)
defer cancel()
currentState := conn.GetState()
timeoutValid := true
for currentState != connectivity.Ready && timeoutValid {
timeoutValid = conn.WaitForStateChange(ctx, currentState)
currentState = conn.GetState()
// debug print currentState -> prints IDLE
}
return currentState == connectivity.Ready
}
Debugging hints also welcome :)
Based on the provided code and information, there might be an issue with how ctx.Done is being utilized.
The ctx.Done() is being used in fooUpdater and goroutineOnefunctions. When connection breaks, I believe that the ctx.Done() gets called in both functions, with the following execution order:
Connection breaks, the ctx.Done case in the fooUpdater function gets called, exiting the function. The select statement in the goroutineOne function also executes the ctx.Done case, which exists the function, and the client doesn't reconnect.
Try debugging it to check if both select case blocks get executed, but I believe that is the issue here.
According to the GRPC documentation, the connection is re-established if there is a transient failure otherwise it fails immediately. You can try to verify that the failure is transient by printing the connectivity state.
You should print the error code also to understand Why RPC failed.
Maybe what you have tried is not considered a transient failure.
Also, according to the following entry retry logic does not work with streams: grpc-java: Proper handling of retry on client for service streaming call
Here are the links to the corresponding docs:
https://grpc.github.io/grpc/core/md_doc_connectivity-semantics-and-api.html
https://pkg.go.dev/google.golang.org/grpc#section-readme
Also, check the following entry:
Ways to wait if server is not available in gRPC from client side
I have a pubsub subscription (all default settings except the number of go-routines is 1000), and for some reason messages never get acknowledged, and therefore redelivered. Redelivery is taking between 1 and 2 minutes. I'm calling message.Ack() less than 1 second after the message is received, so I don't understand what is happening. It shouldn't be because of latency between the app and pubsub itself, because after publishing a message to the topic, the message is delivered practically immediately.
The subscription has an acknowledgement deadline of 10 seconds. I tried increasing this to 120, but the same problem still occurred. I can't think of any reason why these messages aren't being acknowledged, and therefore being redelivered.
Code for reference:
if err := pubsubSubscription(client).Receive(ctx, func(lctx context.Context, message *pubsub.Message) {
log.Println("Received message") // occurs < 1s after publishing
ack := message.Ack
if err := adapters.Handle(conn, id, gatewayAddr, message.Data); err != nil {
log.Println("Will nack message")
ack = message.Nack // not reached (in this context/example)
cancel()
}
log.Println("Will ack message") // occurs ~200µs after message receipt
ack()
}); err != nil {
return fmt.Errorf("unable to subscribe to PubSub messages: %s", err)
}
To clarify, I've only published 1 message to the topic, but that callback is called every 1 or 2 minutes infinitely.
EDIT
This only occurs when the number of go-routines in the subscription receive settings is set to a number higher than runtime.NumCPU(). Is this the expected behaviour? If so, how does this work with Kubernetes (which I'm using)?
EDIT 2 -- request for full code for reproduction
const (
DefaultMaxOutstandingMessages = 1000000
DefaultMaxOutstandingBytes = 1e9
)
func SubscribeToTables(id int) error {
var opts []option.ClientOption
if sa := os.Getenv("SERVICE_ACCOUNT"); sa != "" {
opts = append(opts, option.WithCredentialsJSON([]byte(sa)))
}
ctx := context.Background()
projectID := os.Getenv("PROJECT_ID")
client, err := pubsub.NewClient(ctx, projectID, opts...)
if err != nil {
return fmt.Errorf("error creating GCP PubSub client: %s", err)
}
cctx, cancel := context.WithCancel(ctx)
go func() {
qch := make(chan os.Signal)
signal.Notify(qch, os.Interrupt, syscall.SIGTERM)
<-qch
cancel()
}()
mch := make(chan *pubsub.Message)
gatewayAddr := os.Getenv("GATEWAY_ADDRESS")
conn, err := adapters.GetGatewayConn(gatewayAddr)
if err != nil {
return fmt.Errorf("unable to connect to Gateway: %s", err)
}
go func() {
for {
select {
case message := <-mch:
if err := adapters.Handle(conn, id, gatewayAddr, message.Data); err != nil {
cancel()
return
}
message.Ack()
case <-ctx.Done():
return
}
}
}()
if err := pubsubSubscription(client).Receive(cctx, func(_ context.Context, message *pubsub.Message) {
mch <- message
}); err != nil {
return fmt.Errorf("unable to subscribe to PubSub messages: %s", err)
}
return nil
}
func pubsubSubscription(client *pubsub.Client) *pubsub.Subscription {
sub := client.Subscription(os.Getenv("SUBSCRIPTION_ID"))
sub.ReceiveSettings = pubsub.ReceiveSettings{
MaxExtension: pubsub.DefaultReceiveSettings.MaxExtension,
MaxExtensionPeriod: pubsub.DefaultReceiveSettings.MaxExtensionPeriod,
MaxOutstandingMessages: parsePubSubReceiveSetting(
"MAX_OUTSTANDING_MESSAGES",
"max outstanding messages",
DefaultMaxOutstandingMessages,
),
MaxOutstandingBytes: parsePubSubReceiveSetting(
"MAX_OUTSTANDING_BYTES",
"max outstanding bytes",
DefaultMaxOutstandingBytes,
),
NumGoroutines: parsePubSubReceiveSetting( // if this is higher than runtimie.NumCPU(), the aforementioned issue occurs
"NUM_GO_ROUTINES",
"Go-routines",
1000,
),
}
return sub
}
func parsePubSubReceiveSetting(env, name string, defaultValue int) int {
e := os.Getenv(env)
i, err := strconv.Atoi(e)
if err != nil {
log.Printf("Unable to parse number of GCP PubSub %s. Can't parse '%s' as int", name, e)
log.Printf("Using default number of %s (%d)", name, defaultValue)
return defaultValue
}
return i
}
I suspect that you exit too quickly your code. You have to cancel() the context for stopping the Receive loop and flushing the data back to PubSub.
Try to add cancel() just after your ack()
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
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
I want to create a Websocket connection via GO. This connection follows a clearly defined pattern: The client should "authenticate" (enter data) themself immediately after creating the connection. If the client does not do it, the connection will be closed after a short period.
My current code contains this initial timeout (initTimeout) and the maximum timeout for all connections. While those timers can easily be checked, i am not sure how i can combine the timers with waiting for a message which blocks the execution.
ws, err := upgrader.Upgrade(w, r, nil)
initTimeout := time.NewTicker(time.Duration(30) * time.Second)
maxTimeout := time.NewTicker(time.Duration(45) * time.Minute)
for {
select {
case <- initTimeout.C:
ws.WriteMessage(websocket.TextMessage, []byte("No input received"))
ws.Close()
case <- maxTimeout.C:
ws.WriteMessage(websocket.TextMessage, []byte("Maximum timeout"))
ws.Close()
default:
mt, message, err := c.ReadMessage()
// will this block the timers?
}
}
Use the read deadline to implement the timeouts:
ws, err := upgrader.Upgrade(w, r, nil)
if err != nil {
// handle error
}
// Read the initial message with deadline of 30 seconds
ws.SetReadDeadline(time.Now().Add(30 * time.Second))
mt, message, err := ws.ReadMessage()
if err != nil {
// Handle the error which might be a deadline exceeded error.
}
// process the initial message
// ...
for {
// Read next message with deadline of 45 minutes
ws.SetReadDeadline(time.Now().Add(45 * time.Minute))
mt, message, err = ws.ReadMessage()
if err != nil {
// Handle the error which might be a deadline exceeded error.
}
// process message
// ....
}