commit
1f205e32c8
@ -0,0 +1,21 @@ |
||||
MIT License |
||||
|
||||
Copyright (c) 2022 Valery Stadchenko |
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy |
||||
of this software and associated documentation files (the "Software"), to deal |
||||
in the Software without restriction, including without limitation the rights |
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
||||
copies of the Software, and to permit persons to whom the Software is |
||||
furnished to do so, subject to the following conditions: |
||||
|
||||
The above copyright notice and this permission notice shall be included in all |
||||
copies or substantial portions of the Software. |
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
||||
SOFTWARE. |
||||
@ -0,0 +1,12 @@ |
||||
module github.com/PCManiac/rabbit_reconnector |
||||
|
||||
go 1.17 |
||||
|
||||
require ( |
||||
github.com/PCManiac/logrus_init v0.0.0 |
||||
github.com/caarlos0/env/v6 v6.10.0 // indirect |
||||
github.com/google/uuid v1.3.0 |
||||
github.com/sirupsen/logrus v1.9.0 |
||||
github.com/streadway/amqp v1.0.0 |
||||
golang.org/x/sys v0.0.0-20220829200755-d48e67d00261 // indirect |
||||
) |
||||
@ -0,0 +1,31 @@ |
||||
github.com/PCManiac/logrus_init v0.0.0 h1:sn8aQyHLmN/AMTkZfKCQo4mOB5Ckhs+FL85ygNybyMw= |
||||
github.com/PCManiac/logrus_init v0.0.0/go.mod h1:mDbCY64MUIrl+gcI37TQGpb22ADZlY8WBJ6tR2pteqk= |
||||
github.com/caarlos0/env/v6 v6.10.0 h1:lA7sxiGArZ2KkiqpOQNf8ERBRWI+v8MWIH+eGjSN22I= |
||||
github.com/caarlos0/env/v6 v6.10.0/go.mod h1:hvp/ryKXKipEkcuYjs9mI4bBCg+UI0Yhgm5Zu0ddvwc= |
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= |
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= |
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= |
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= |
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= |
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= |
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= |
||||
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= |
||||
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= |
||||
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= |
||||
github.com/streadway/amqp v1.0.0 h1:kuuDrUJFZL1QYL9hUNuCxNObNzB0bV/ZG5jV3RWAQgo= |
||||
github.com/streadway/amqp v1.0.0/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= |
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= |
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= |
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= |
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= |
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= |
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= |
||||
golang.org/x/sys v0.0.0-20211109184856-51b60fd695b3/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= |
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= |
||||
golang.org/x/sys v0.0.0-20220829200755-d48e67d00261 h1:v6hYoSR9T5oet+pMXwUWkbiVqx/63mlHjefrHmxwfeY= |
||||
golang.org/x/sys v0.0.0-20220829200755-d48e67d00261/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= |
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= |
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= |
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= |
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= |
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= |
||||
@ -0,0 +1,123 @@ |
||||
package rabbit_reconnector |
||||
|
||||
import ( |
||||
"errors" |
||||
"time" |
||||
|
||||
_ "github.com/PCManiac/logrus_init" |
||||
log "github.com/sirupsen/logrus" |
||||
"github.com/streadway/amqp" |
||||
) |
||||
|
||||
type Reconnector interface { |
||||
RabbitConnect() |
||||
GetConnection() *amqp.Connection |
||||
SubscribeTo(exhangeName string, queueName string, routingKey string) (<-chan amqp.Delivery, *amqp.Channel, error) |
||||
publishResponse(ch *amqp.Channel, exchangeName string, replyTo string, correlationId string, body string, sessionId string, content_type string) (err error) |
||||
executeRPC(ch *amqp.Channel, exhangeName string, body []byte, strTimeout string, routingKey string) (response []byte, err error) |
||||
publishTo(ch *amqp.Channel, exhangeName string, routingKey string, body string) (err error) |
||||
} |
||||
|
||||
type ReconnectorEventHandler interface { |
||||
AfterReconnect(AmqpConnection *amqp.Connection, ExitSignal chan bool) error |
||||
} |
||||
|
||||
type server struct { |
||||
AmqpConnection *amqp.Connection |
||||
AmqpCloseError chan *amqp.Error |
||||
ExitSignal chan bool |
||||
amqpHostName string |
||||
handler ReconnectorEventHandler |
||||
} |
||||
|
||||
func (s *server) DoRabbitConnect() (err error) { |
||||
log.WithFields(log.Fields{ |
||||
"proc": "RabbitConnect", |
||||
}).Warn("RabbitConnect started") |
||||
|
||||
var rabbit *amqp.Connection |
||||
|
||||
for i := 0; i < 60; i++ { |
||||
rabbit, err = amqp.Dial(s.amqpHostName) |
||||
if err != nil { |
||||
log.WithFields(log.Fields{ |
||||
"error": err, |
||||
"stage": "dial", |
||||
"proc": "RabbitConnect", |
||||
}).Error("AMQP error") |
||||
|
||||
time.Sleep(500 * time.Millisecond) |
||||
continue |
||||
} |
||||
|
||||
s.AmqpConnection = rabbit |
||||
|
||||
go func() { |
||||
<-rabbit.NotifyClose(make(chan *amqp.Error)) |
||||
log.WithFields(log.Fields{ |
||||
"proc": "RabbitConnect goroutine", |
||||
}).Error("NotifyClose received. Sending to reconnect.") |
||||
s.AmqpCloseError <- amqp.ErrClosed |
||||
}() |
||||
|
||||
if s.handler != nil { |
||||
s.ExitSignal = make(chan bool, 1) |
||||
if err := s.handler.AfterReconnect(s.AmqpConnection, s.ExitSignal); err != nil { |
||||
log.WithFields(log.Fields{ |
||||
"error": err, |
||||
"proc": "RabbitConnect", |
||||
}).Error("Failed to AfterReconnect") |
||||
return err |
||||
} |
||||
} |
||||
|
||||
log.WithFields(log.Fields{ |
||||
"proc": "RabbitConnect", |
||||
}).Warn("RabbitConnect Ok") |
||||
return nil |
||||
} |
||||
|
||||
log.WithFields(log.Fields{ |
||||
"proc": "RabbitConnect", |
||||
}).Error("AMQP connect error") |
||||
return errors.New("amqp connect error") |
||||
} |
||||
|
||||
func (s *server) RabbitConnect() { |
||||
for { |
||||
<-s.AmqpCloseError |
||||
|
||||
log.WithFields(log.Fields{ |
||||
"proc": "RabbitReConnector", |
||||
}).Error("amqpCloseError received. Reconnecting.") |
||||
|
||||
s.ExitSignal <- true |
||||
|
||||
err := s.DoRabbitConnect() |
||||
if err != nil { |
||||
log.WithFields(log.Fields{ |
||||
"error": err, |
||||
"stage": "RabbitConnect", |
||||
"proc": "RabbitReConnector", |
||||
}).Panic("AMQP error") |
||||
} |
||||
} |
||||
} |
||||
|
||||
func (s *server) GetConnection() *amqp.Connection { |
||||
return s.AmqpConnection |
||||
} |
||||
|
||||
func New(amqpHost string, handler ReconnectorEventHandler) Reconnector { |
||||
s := server{ |
||||
AmqpCloseError: make(chan *amqp.Error), |
||||
amqpHostName: amqpHost, |
||||
ExitSignal: make(chan bool, 1), |
||||
} |
||||
|
||||
if handler != nil { |
||||
s.handler = handler |
||||
} |
||||
|
||||
return &s |
||||
} |
||||
@ -0,0 +1,310 @@ |
||||
package rabbit_reconnector |
||||
|
||||
import ( |
||||
"errors" |
||||
"time" |
||||
|
||||
_ "github.com/PCManiac/logrus_init" |
||||
"github.com/google/uuid" |
||||
log "github.com/sirupsen/logrus" |
||||
"github.com/streadway/amqp" |
||||
) |
||||
|
||||
func (s *server) SubscribeTo(exhangeName string, queueName string, routingKey string) (<-chan amqp.Delivery, *amqp.Channel, error) { |
||||
ch, err := s.AmqpConnection.Channel() |
||||
if err != nil { |
||||
log.WithFields(log.Fields{ |
||||
"error": err, |
||||
"stage": "Channel create", |
||||
"proc": "RabbitConnect", |
||||
}).Error("AMQP error") |
||||
return nil, nil, err |
||||
} |
||||
|
||||
err = ch.ExchangeDeclare( |
||||
exhangeName, // name
|
||||
"direct", // type
|
||||
true, // durable
|
||||
true, // auto-deleted
|
||||
false, // internal
|
||||
false, // no-wait
|
||||
nil, // arguments
|
||||
) |
||||
if err != nil { |
||||
log.WithFields(log.Fields{ |
||||
"error": err, |
||||
"exchange": exhangeName, |
||||
"stage": "exchange declare", |
||||
}).Error("AMQP error.") |
||||
return nil, nil, err |
||||
} |
||||
|
||||
q, err := ch.QueueDeclare( |
||||
queueName, // name
|
||||
true, // durable
|
||||
true, // delete when unused
|
||||
queueName != "", // exclusive
|
||||
false, // no-wait
|
||||
nil, // arguments
|
||||
) |
||||
if err != nil { |
||||
log.WithFields(log.Fields{ |
||||
"error": err, |
||||
"exchange": exhangeName, |
||||
"stage": "queue declare", |
||||
}).Error("AMQP error.") |
||||
return nil, nil, err |
||||
} |
||||
err = ch.QueueBind( |
||||
q.Name, // queue name
|
||||
routingKey, // routing key
|
||||
exhangeName, // exchange
|
||||
false, |
||||
nil, |
||||
) |
||||
if err != nil { |
||||
log.WithFields(log.Fields{ |
||||
"error": err, |
||||
"exchange": exhangeName, |
||||
"stage": "bind", |
||||
}).Error("AMQP error.") |
||||
return nil, nil, err |
||||
} |
||||
msgs, err := ch.Consume( |
||||
q.Name, // queue
|
||||
"", // consumer
|
||||
true, // auto-ack
|
||||
queueName != "", // exclusive
|
||||
false, // no-local
|
||||
false, // no-wait
|
||||
nil, // args
|
||||
) |
||||
if err != nil { |
||||
log.WithFields(log.Fields{ |
||||
"error": err, |
||||
"exchange": exhangeName, |
||||
"stage": "consume", |
||||
}).Error("AMQP error.") |
||||
return nil, nil, err |
||||
} |
||||
return msgs, ch, nil |
||||
} |
||||
|
||||
func (s *server) publishResponse(ch *amqp.Channel, exchangeName string, replyTo string, correlationId string, body string, sessionId string, content_type string) (err error) { |
||||
err1 := ch.Publish( |
||||
exchangeName, // exchange
|
||||
replyTo, // routing key
|
||||
false, // mandatory
|
||||
false, // immediate
|
||||
amqp.Publishing{ |
||||
ContentType: content_type, |
||||
CorrelationId: correlationId, |
||||
Body: []byte(body), |
||||
}) |
||||
|
||||
if err1 != nil { |
||||
log.WithFields(log.Fields{ |
||||
"error": err1, |
||||
"session_id": sessionId, |
||||
"proc": "publishResponse", |
||||
}).Error("Failed to publish to RPC") |
||||
return err |
||||
} else { |
||||
log.WithFields(log.Fields{ |
||||
"body": body, |
||||
"session_id": sessionId, |
||||
"proc": "publishResponse", |
||||
"exchange": exchangeName, |
||||
}).Trace("Published to RPC") |
||||
return nil |
||||
} |
||||
} |
||||
|
||||
func (s *server) executeRPC(ch *amqp.Channel, exhangeName string, body []byte, strTimeout string, routingKey string) (response []byte, err error) { |
||||
err = ch.ExchangeDeclare( |
||||
exhangeName, // name
|
||||
"direct", // type
|
||||
true, // durable
|
||||
true, // auto-deleted
|
||||
false, // internal
|
||||
false, // no-wait
|
||||
nil, // arguments
|
||||
) |
||||
if err != nil { |
||||
log.WithFields(log.Fields{ |
||||
"error": err, |
||||
"exchange": exhangeName, |
||||
"stage": "exchange declare", |
||||
"proc": "executeRPC", |
||||
}).Error("AMQP error.") |
||||
return nil, err |
||||
} |
||||
|
||||
q, err := ch.QueueDeclare( |
||||
"", // name
|
||||
true, // durable
|
||||
true, // delete when unused
|
||||
true, // exclusive
|
||||
false, // no-wait
|
||||
nil, // arguments
|
||||
) |
||||
if err != nil { |
||||
log.WithFields(log.Fields{ |
||||
"error": err, |
||||
"exchange": exhangeName, |
||||
"stage": "queue declare", |
||||
"proc": "executeRPC", |
||||
}).Error("AMQP error.") |
||||
return nil, err |
||||
} |
||||
|
||||
err = ch.QueueBind( |
||||
q.Name, // queue name
|
||||
q.Name, // routing key
|
||||
exhangeName, // exchange
|
||||
false, |
||||
nil) |
||||
if err != nil { |
||||
log.WithFields(log.Fields{ |
||||
"error": err, |
||||
"exchange": exhangeName, |
||||
"stage": "bind", |
||||
"proc": "executeRPC", |
||||
}).Error("AMQP error.") |
||||
return nil, err |
||||
} |
||||
|
||||
corrID := uuid.NewString() |
||||
|
||||
err = ch.Publish( |
||||
exhangeName, // exchange
|
||||
routingKey, // routing key
|
||||
false, // mandatory
|
||||
false, // immediate
|
||||
amqp.Publishing{ |
||||
ContentType: "text/plain", |
||||
CorrelationId: corrID, |
||||
ReplyTo: q.Name, |
||||
Body: body, |
||||
}) |
||||
|
||||
if err != nil { |
||||
log.WithFields(log.Fields{ |
||||
"error": err, |
||||
"exchange": exhangeName, |
||||
"stage": "publish", |
||||
"proc": "executeRPC", |
||||
}).Error("AMQP error.") |
||||
return nil, err |
||||
} else { |
||||
log.WithFields(log.Fields{ |
||||
"body": body, |
||||
"exchange": exhangeName, |
||||
"stage": "publish", |
||||
"proc": "executeRPC", |
||||
}).Trace("RPC published") |
||||
} |
||||
|
||||
msgs, err := ch.Consume( |
||||
q.Name, // queue
|
||||
"", // consumer
|
||||
true, // auto-ack
|
||||
true, // exclusive
|
||||
false, // no-local
|
||||
false, // no-wait
|
||||
nil, // args
|
||||
) |
||||
if err != nil { |
||||
log.WithFields(log.Fields{ |
||||
"error": err, |
||||
"exchange": exhangeName, |
||||
"stage": "consume", |
||||
"proc": "executeRPC", |
||||
}).Error("AMQP error.") |
||||
return nil, err |
||||
} |
||||
|
||||
timeout_interval, errt := time.ParseDuration(strTimeout) |
||||
if errt != nil { |
||||
timeout_interval, _ = time.ParseDuration("10s") |
||||
log.WithFields(log.Fields{ |
||||
"config_timeout": strTimeout, |
||||
"proc": "executeRPC", |
||||
}).Warn("Failed to parse timeout") |
||||
} |
||||
|
||||
timeout := time.After(timeout_interval) |
||||
for { |
||||
select { |
||||
case d := <-msgs: |
||||
if corrID == d.CorrelationId { |
||||
res := string(d.Body) |
||||
|
||||
log.WithFields(log.Fields{ |
||||
"response": res, |
||||
"exchange": exhangeName, |
||||
"stage": "for loop", |
||||
"proc": "executeRPC", |
||||
}).Trace("AMQP RPC response") |
||||
|
||||
return d.Body, nil |
||||
} |
||||
return |
||||
case <-timeout: |
||||
log.WithFields(log.Fields{ |
||||
"error": "AMQP RPC timeout", |
||||
"exchange": exhangeName, |
||||
"stage": "for loop", |
||||
"proc": "executeRPC", |
||||
}).Error("AMQP timeout") |
||||
return nil, errors.New("timeout") |
||||
} |
||||
} |
||||
} |
||||
|
||||
func (s *server) publishTo(ch *amqp.Channel, exhangeName string, routingKey string, body string) (err error) { |
||||
err = ch.ExchangeDeclare( |
||||
exhangeName, // name
|
||||
"direct", // type
|
||||
true, // durable
|
||||
true, // auto-deleted
|
||||
false, // internal
|
||||
false, // no-wait
|
||||
nil, // arguments
|
||||
) |
||||
if err != nil { |
||||
log.WithFields(log.Fields{ |
||||
"error": err, |
||||
"exchange": exhangeName, |
||||
"stage": "exchange declare", |
||||
}).Error("AMQP error.") |
||||
return err |
||||
} |
||||
|
||||
err = ch.Publish( |
||||
exhangeName, // exchange
|
||||
routingKey, // routing key
|
||||
false, // mandatory
|
||||
false, // immediate
|
||||
amqp.Publishing{ |
||||
ContentType: "text/plain", |
||||
Body: []byte(body), |
||||
}) |
||||
|
||||
if err != nil { |
||||
log.WithFields(log.Fields{ |
||||
"error": err, |
||||
"exchange": exhangeName, |
||||
"stage": "publish", |
||||
}).Error("AMQP error.") |
||||
return err |
||||
} else { |
||||
log.WithFields(log.Fields{ |
||||
"body": body, |
||||
"exchange": exhangeName, |
||||
"stage": "publish", |
||||
}).Trace("Data published") |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
Loading…
Reference in new issue