1
0
mirror of https://github.com/eventials/goevents.git synced 2025-04-24 13:48:53 +08:00

Use interface to allow mocking.

This commit is contained in:
Alexandre Vicenzi 2016-12-01 10:52:22 -02:00
parent 4477dc24e8
commit 144d3870f6
8 changed files with 334 additions and 275 deletions

1
.gitignore vendored
View File

@ -22,3 +22,4 @@ _testmain.go
*.exe
*.test
*.prof
.DS_Store

77
amqp/connection.go Normal file
View File

@ -0,0 +1,77 @@
package amqp
import (
base "github.com/eventials/goevents"
amqplib "github.com/streadway/amqp"
)
type Connection struct {
connection *amqplib.Connection
channel *amqplib.Channel
queue *amqplib.Queue
exchangeName string
queueName string
}
func NewConnection(url, exchange, queue string) (base.Connection, error) {
conn, err := amqplib.Dial(url)
if err != nil {
return nil, err
}
ch, err := conn.Channel()
if err != nil {
return nil, err
}
err = ch.ExchangeDeclare(
exchange, // name
"topic", // type
true, // durable
false, // auto-delete
false, // internal
false, // no-wait
nil, // arguments
)
if err != nil {
return nil, err
}
q, err := ch.QueueDeclare(
queue, // name
true, // durable
false, // auto-delete
false, // exclusive
false, // no-wait
nil, // arguments
)
if err != nil {
return nil, err
}
return &Connection{
conn,
ch,
&q,
exchange,
queue,
}, nil
}
func (c *Connection) Consumer(autoAck bool) (base.Consumer, error) {
return NewConsumer(c, autoAck)
}
func (c *Connection) Producer() (base.Producer, error) {
return NewProducer(c)
}
func (c *Connection) Close() {
c.channel.Close()
c.connection.Close()
}

165
amqp/consumer.go Normal file
View File

@ -0,0 +1,165 @@
package amqp
import (
"regexp"
"strings"
base "github.com/eventials/goevents"
amqplib "github.com/streadway/amqp"
)
type handler struct {
action string
handler base.EventHandler
re *regexp.Regexp
}
type Consumer struct {
conn *Connection
autoAck bool
handlers []handler
}
func NewConsumer(c base.Connection, autoAck bool) (base.Consumer, error) {
amqpConn := c.(*Connection)
return &Consumer{
amqpConn,
autoAck,
make([]handler, 0),
}, nil
}
func (c *Consumer) dispatch(msg amqplib.Delivery) {
if fn, ok := c.getHandler(msg.RoutingKey); ok {
defer func() {
if err := recover(); err != nil {
if !c.autoAck {
msg.Nack(false, true)
}
}
}()
ok := fn(msg.Body)
if !c.autoAck {
if ok {
msg.Ack(false)
} else {
msg.Nack(false, true)
}
}
} else {
// got a message from wrong exchange?
// ignore and don't requeue.
msg.Nack(false, false)
}
}
func (c *Consumer) getHandler(action string) (base.EventHandler, bool) {
for _, h := range c.handlers {
if h.re.MatchString(action) {
return h.handler, true
}
}
return nil, false
}
func (c *Consumer) Subscribe(action string, handlerFn base.EventHandler) error {
// TODO: Replace # pattern too.
pattern := strings.Replace(action, "*", "(.*)", 0)
re, err := regexp.Compile(pattern)
if err != nil {
return err
}
for _, h := range c.handlers {
if h.action == action {
// return fmt.Errorf("Action '%s' already registered.", action)
}
}
err = c.conn.channel.QueueBind(
c.conn.queueName, // queue name
action, // routing key
c.conn.exchangeName, // exchange
false, // no-wait
nil, // arguments
)
if err != nil {
return err
}
c.handlers = append(c.handlers, handler{
action,
handlerFn,
re,
})
return nil
}
func (c *Consumer) Unsubscribe(action string) error {
err := c.conn.channel.QueueUnbind(
c.conn.queueName, // queue name
action, // routing key
c.conn.exchangeName, // exchange
nil, // arguments
)
if err != nil {
return err
}
idx := -1
for i, h := range c.handlers {
if h.action == action {
idx = i
break
}
}
if idx != -1 {
c.handlers = append(c.handlers[:idx], c.handlers[idx+1:]...)
}
return nil
}
func (c *Consumer) Listen() error {
msgs, err := c.conn.channel.Consume(
c.conn.queueName, // queue
"", // consumer
c.autoAck, // auto ack
false, // exclusive
false, // no local
false, // no wait
nil, // args
)
if err != nil {
return err
}
go func() {
for m := range msgs {
c.dispatch(m)
}
}()
return nil
}
func (c *Consumer) ListenForever() error {
err := c.Listen()
if err != nil {
return err
}
select {}
}

View File

@ -1,4 +1,4 @@
package events
package amqp
import (
"testing"
@ -18,9 +18,12 @@ func TestPublishConsume(t *testing.T) {
defer conn.Close()
// Clean all messages if any...
conn.channel.QueuePurge(conn.queueName, false)
amqpConn := conn.(*Connection)
amqpConn.channel.QueuePurge(amqpConn.queueName, false)
c := NewConsumer(conn, false)
c, err := NewConsumer(conn, false)
assert.Nil(t, err)
c.Subscribe("my_action_1", func(body []byte) bool {
func1 <- true
@ -34,7 +37,9 @@ func TestPublishConsume(t *testing.T) {
c.Listen()
p := NewProducer(conn)
p, err := NewProducer(conn)
assert.Nil(t, err)
err = p.Publish("my_action_1", []byte(""))
@ -60,9 +65,12 @@ func TestPublishConsumeWildcardAction(t *testing.T) {
defer conn.Close()
// Clean all messages if any...
conn.channel.QueuePurge(conn.queueName, false)
amqpConn := conn.(*Connection)
amqpConn.channel.QueuePurge(amqpConn.queueName, false)
c := NewConsumer(conn, false)
c, err := NewConsumer(conn, false)
assert.Nil(t, err)
c.Subscribe("webinar.*", func(body []byte) bool {
func1 <- true
@ -76,7 +84,9 @@ func TestPublishConsumeWildcardAction(t *testing.T) {
c.Listen()
p := NewProducer(conn)
p, err := NewProducer(conn)
assert.Nil(t, err)
err = p.Publish("webinar.state_changed", []byte(""))
@ -102,9 +112,12 @@ func TestPublishConsumeWildcardActionOrderMatters1(t *testing.T) {
defer conn.Close()
// Clean all messages if any...
conn.channel.QueuePurge(conn.queueName, false)
amqpConn := conn.(*Connection)
amqpConn.channel.QueuePurge(amqpConn.queueName, false)
c := NewConsumer(conn, false)
c, err := NewConsumer(conn, false)
assert.Nil(t, err)
c.Subscribe("webinar.*", func(body []byte) bool {
func1 <- true
@ -118,7 +131,9 @@ func TestPublishConsumeWildcardActionOrderMatters1(t *testing.T) {
c.Listen()
p := NewProducer(conn)
p, err := NewProducer(conn)
assert.Nil(t, err)
err = p.Publish("webinar.state_changed", []byte(""))
@ -144,9 +159,12 @@ func TestPublishConsumeWildcardActionOrderMatters2(t *testing.T) {
defer conn.Close()
// Clean all messages if any...
conn.channel.QueuePurge(conn.queueName, false)
amqpConn := conn.(*Connection)
amqpConn.channel.QueuePurge(amqpConn.queueName, false)
c := NewConsumer(conn, false)
c, err := NewConsumer(conn, false)
assert.Nil(t, err)
c.Subscribe("webinar.state_changed", func(body []byte) bool {
func1 <- true
@ -160,7 +178,9 @@ func TestPublishConsumeWildcardActionOrderMatters2(t *testing.T) {
c.Listen()
p := NewProducer(conn)
p, err := NewProducer(conn)
assert.Nil(t, err)
err = p.Publish("webinar.state_changed", []byte(""))
@ -186,9 +206,12 @@ func TestPublishConsumeRequeueIfFail(t *testing.T) {
defer conn.Close()
// Clean all messages if any...
conn.channel.QueuePurge(conn.queueName, false)
amqpConn := conn.(*Connection)
amqpConn.channel.QueuePurge(amqpConn.queueName, false)
c := NewConsumer(conn, false)
c, err := NewConsumer(conn, false)
assert.Nil(t, err)
c.Subscribe("my_action", func(body []byte) bool {
if calledOnce {
@ -202,7 +225,9 @@ func TestPublishConsumeRequeueIfFail(t *testing.T) {
c.Listen()
p := NewProducer(conn)
p, err := NewProducer(conn)
assert.Nil(t, err)
err = p.Publish("my_action", []byte(""))
@ -226,9 +251,12 @@ func TestPublishConsumeRequeueIfPanic(t *testing.T) {
defer conn.Close()
// Clean all messages if any...
conn.channel.QueuePurge(conn.queueName, false)
amqpConn := conn.(*Connection)
amqpConn.channel.QueuePurge(amqpConn.queueName, false)
c := NewConsumer(conn, false)
c, err := NewConsumer(conn, false)
assert.Nil(t, err)
c.Subscribe("my_action", func(body []byte) bool {
if calledOnce {
@ -242,7 +270,9 @@ func TestPublishConsumeRequeueIfPanic(t *testing.T) {
c.Listen()
p := NewProducer(conn)
p, err := NewProducer(conn)
assert.Nil(t, err)
err = p.Publish("my_action", []byte(""))

30
amqp/producer.go Normal file
View File

@ -0,0 +1,30 @@
package amqp
import (
"time"
base "github.com/eventials/goevents"
amqplib "github.com/streadway/amqp"
)
type Producer struct {
conn *Connection
}
func NewProducer(c base.Connection) (base.Producer, error) {
amqpConn := c.(*Connection)
return &Producer{
amqpConn,
}, nil
}
func (p *Producer) Publish(action string, data []byte) error {
msg := amqplib.Publishing{
DeliveryMode: amqplib.Persistent,
Timestamp: time.Now(),
Body: data,
}
return p.conn.channel.Publish(p.conn.exchangeName, action, false, false, msg)
}

View File

@ -1,76 +1,7 @@
package events
import (
"github.com/streadway/amqp"
)
type Connection struct {
connection *amqp.Connection
channel *amqp.Channel
queue *amqp.Queue
exchangeName string
queueName string
}
func NewConnection(url, exchange, queue string) (*Connection, error) {
conn, err := amqp.Dial(url)
if err != nil {
return nil, err
}
ch, err := conn.Channel()
if err != nil {
return nil, err
}
err = ch.ExchangeDeclare(
exchange, // name
"topic", // type
true, // durable
false, // auto-delete
false, // internal
false, // no-wait
nil, // arguments
)
if err != nil {
return nil, err
}
q, err := ch.QueueDeclare(
queue, // name
true, // durable
false, // auto-delete
false, // exclusive
false, // no-wait
nil, // arguments
)
if err != nil {
return nil, err
}
return &Connection{
conn,
ch,
&q,
exchange,
queue,
}, nil
}
func (c *Connection) Consumer(autoAck bool) *Consumer {
return NewConsumer(c, autoAck)
}
func (c *Connection) Producer() *Producer {
return NewProducer(c)
}
func (c *Connection) Close() {
c.channel.Close()
c.connection.Close()
type Connection interface {
Consumer(autoAck bool) (Consumer, error)
Producer() (Producer, error)
Close()
}

View File

@ -1,163 +1,10 @@
package events
import (
"github.com/streadway/amqp"
"regexp"
"strings"
)
type EventHandler func(body []byte) bool
type eventHandler func(body []byte) bool
type handler struct {
action string
handler eventHandler
re *regexp.Regexp
}
type Consumer struct {
conn *Connection
autoAck bool
handlers []handler
}
func NewConsumer(c *Connection, autoAck bool) *Consumer {
return &Consumer{
c,
autoAck,
make([]handler, 0),
}
}
func (c *Consumer) dispatch(msg amqp.Delivery) {
if fn, ok := c.getHandler(msg.RoutingKey); ok {
defer func() {
if err := recover(); err != nil {
if !c.autoAck {
msg.Nack(false, true)
}
}
}()
ok := fn(msg.Body)
if !c.autoAck {
if ok {
msg.Ack(false)
} else {
msg.Nack(false, true)
}
}
} else {
// got a message from wrong exchange?
// ignore and don't requeue.
msg.Nack(false, false)
}
}
func (c *Consumer) getHandler(action string) (eventHandler, bool) {
for _, h := range c.handlers {
if h.re.MatchString(action) {
return h.handler, true
}
}
return nil, false
}
func (c *Consumer) Subscribe(action string, handlerFn eventHandler) error {
// TODO: Replace # pattern too.
pattern := strings.Replace(action, "*", "(.*)", 0)
re, err := regexp.Compile(pattern)
if err != nil {
return err
}
for _, h := range c.handlers {
if h.action == action {
// return fmt.Errorf("Action '%s' already registered.", action)
}
}
err = c.conn.channel.QueueBind(
c.conn.queueName, // queue name
action, // routing key
c.conn.exchangeName, // exchange
false, // no-wait
nil, // arguments
)
if err != nil {
return err
}
c.handlers = append(c.handlers, handler{
action,
handlerFn,
re,
})
return nil
}
func (c *Consumer) Unsubscribe(action string) error {
err := c.conn.channel.QueueUnbind(
c.conn.queueName, // queue name
action, // routing key
c.conn.exchangeName, // exchange
nil, // arguments
)
if err != nil {
return err
}
idx := -1
for i, h := range c.handlers {
if h.action == action {
idx = i
break
}
}
if idx != -1 {
c.handlers = append(c.handlers[:idx], c.handlers[idx+1:]...)
}
return nil
}
func (c *Consumer) Listen() error {
msgs, err := c.conn.channel.Consume(
c.conn.queueName, // queue
"", // consumer
c.autoAck, // auto ack
false, // exclusive
false, // no local
false, // no wait
nil, // args
)
if err != nil {
return err
}
go func() {
for m := range msgs {
c.dispatch(m)
}
}()
return nil
}
func (c *Consumer) ListenForever() error {
err := c.Listen()
if err != nil {
return err
}
select {}
type Consumer interface {
Subscribe(action string, handler EventHandler) error
Unsubscribe(action string) error
Listen() error
ListenForever() error
}

View File

@ -1,27 +1,5 @@
package events
import (
"time"
"github.com/streadway/amqp"
)
type Producer struct {
conn *Connection
}
func NewProducer(c *Connection) *Producer {
return &Producer{
c,
}
}
func (p *Producer) Publish(action string, data []byte) error {
msg := amqp.Publishing{
DeliveryMode: amqp.Persistent,
Timestamp: time.Now(),
Body: data,
}
return p.conn.channel.Publish(p.conn.exchangeName, action, false, false, msg)
type Producer interface {
Publish(action string, data []byte) error
}