mirror of
https://github.com/eventials/goevents.git
synced 2025-04-26 13:48:59 +08:00
Merge pull request #1 from alexandrevicenzi/master
Go messaging library.
This commit is contained in:
commit
4477dc24e8
12
Dockerfile
Normal file
12
Dockerfile
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
FROM golang:1.7
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y wget
|
||||||
|
RUN wget https://github.com/jwilder/dockerize/releases/download/v0.2.0/dockerize-linux-amd64-v0.2.0.tar.gz
|
||||||
|
RUN tar -C /usr/local/bin -xzvf dockerize-linux-amd64-v0.2.0.tar.gz
|
||||||
|
|
||||||
|
RUN mkdir -p /go/src/github.com/eventials/goevents
|
||||||
|
WORKDIR /go/src/github.com/eventials/goevents
|
||||||
|
|
||||||
|
RUN go get github.com/eventials/golog
|
||||||
|
RUN go get github.com/streadway/amqp
|
||||||
|
RUN go get github.com/stretchr/testify
|
76
connection.go
Normal file
76
connection.go
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
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()
|
||||||
|
}
|
163
consumer.go
Normal file
163
consumer.go
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
package events
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/streadway/amqp"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
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 {}
|
||||||
|
}
|
14
docker-compose.yml
Normal file
14
docker-compose.yml
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
version: '2'
|
||||||
|
services:
|
||||||
|
app:
|
||||||
|
build: .
|
||||||
|
working_dir: /go/src/github.com/eventials/goevents
|
||||||
|
command: dockerize -wait tcp://broker:5672 -timeout 60s go run main.go
|
||||||
|
links:
|
||||||
|
- broker
|
||||||
|
volumes:
|
||||||
|
- .:/go/src/github.com/eventials/goevents
|
||||||
|
broker:
|
||||||
|
image: rabbitmq:3.6-management
|
||||||
|
# ports:
|
||||||
|
# - "15672:15672"
|
256
integration_test.go
Normal file
256
integration_test.go
Normal file
@ -0,0 +1,256 @@
|
|||||||
|
package events
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPublishConsume(t *testing.T) {
|
||||||
|
func1 := make(chan bool)
|
||||||
|
func2 := make(chan bool)
|
||||||
|
|
||||||
|
conn, err := NewConnection("amqp://guest:guest@broker:5672/", "event_PublishConsumeer", "webhooks")
|
||||||
|
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
// Clean all messages if any...
|
||||||
|
conn.channel.QueuePurge(conn.queueName, false)
|
||||||
|
|
||||||
|
c := NewConsumer(conn, false)
|
||||||
|
|
||||||
|
c.Subscribe("my_action_1", func(body []byte) bool {
|
||||||
|
func1 <- true
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
c.Subscribe("my_action_2", func(body []byte) bool {
|
||||||
|
func2 <- true
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
c.Listen()
|
||||||
|
|
||||||
|
p := NewProducer(conn)
|
||||||
|
|
||||||
|
err = p.Publish("my_action_1", []byte(""))
|
||||||
|
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-func1:
|
||||||
|
case <-func2:
|
||||||
|
assert.Fail(t, "called wrong action")
|
||||||
|
case <-time.After(5 * time.Second):
|
||||||
|
assert.Fail(t, "timed out")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPublishConsumeWildcardAction(t *testing.T) {
|
||||||
|
func1 := make(chan bool)
|
||||||
|
func2 := make(chan bool)
|
||||||
|
|
||||||
|
conn, err := NewConnection("amqp://guest:guest@broker:5672/", "event_PublishConsumeer", "webhooks")
|
||||||
|
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
// Clean all messages if any...
|
||||||
|
conn.channel.QueuePurge(conn.queueName, false)
|
||||||
|
|
||||||
|
c := NewConsumer(conn, false)
|
||||||
|
|
||||||
|
c.Subscribe("webinar.*", func(body []byte) bool {
|
||||||
|
func1 <- true
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
c.Subscribe("foobar.*", func(body []byte) bool {
|
||||||
|
func2 <- true
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
c.Listen()
|
||||||
|
|
||||||
|
p := NewProducer(conn)
|
||||||
|
|
||||||
|
err = p.Publish("webinar.state_changed", []byte(""))
|
||||||
|
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-func1:
|
||||||
|
case <-func2:
|
||||||
|
assert.Fail(t, "called wrong action")
|
||||||
|
case <-time.After(5 * time.Second):
|
||||||
|
assert.Fail(t, "timed out")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPublishConsumeWildcardActionOrderMatters1(t *testing.T) {
|
||||||
|
func1 := make(chan bool)
|
||||||
|
func2 := make(chan bool)
|
||||||
|
|
||||||
|
conn, err := NewConnection("amqp://guest:guest@broker:5672/", "event_PublishConsumeer", "webhooks")
|
||||||
|
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
// Clean all messages if any...
|
||||||
|
conn.channel.QueuePurge(conn.queueName, false)
|
||||||
|
|
||||||
|
c := NewConsumer(conn, false)
|
||||||
|
|
||||||
|
c.Subscribe("webinar.*", func(body []byte) bool {
|
||||||
|
func1 <- true
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
c.Subscribe("webinar.state_changed", func(body []byte) bool {
|
||||||
|
func2 <- true
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
c.Listen()
|
||||||
|
|
||||||
|
p := NewProducer(conn)
|
||||||
|
|
||||||
|
err = p.Publish("webinar.state_changed", []byte(""))
|
||||||
|
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-func1:
|
||||||
|
case <-func2:
|
||||||
|
assert.Fail(t, "called wrong action")
|
||||||
|
case <-time.After(5 * time.Second):
|
||||||
|
assert.Fail(t, "timed out")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPublishConsumeWildcardActionOrderMatters2(t *testing.T) {
|
||||||
|
func1 := make(chan bool)
|
||||||
|
func2 := make(chan bool)
|
||||||
|
|
||||||
|
conn, err := NewConnection("amqp://guest:guest@broker:5672/", "event_PublishConsumeer", "webhooks")
|
||||||
|
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
// Clean all messages if any...
|
||||||
|
conn.channel.QueuePurge(conn.queueName, false)
|
||||||
|
|
||||||
|
c := NewConsumer(conn, false)
|
||||||
|
|
||||||
|
c.Subscribe("webinar.state_changed", func(body []byte) bool {
|
||||||
|
func1 <- true
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
c.Subscribe("webinar.*", func(body []byte) bool {
|
||||||
|
func2 <- true
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
c.Listen()
|
||||||
|
|
||||||
|
p := NewProducer(conn)
|
||||||
|
|
||||||
|
err = p.Publish("webinar.state_changed", []byte(""))
|
||||||
|
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-func1:
|
||||||
|
case <-func2:
|
||||||
|
assert.Fail(t, "called wrong action")
|
||||||
|
case <-time.After(5 * time.Second):
|
||||||
|
assert.Fail(t, "timed out")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPublishConsumeRequeueIfFail(t *testing.T) {
|
||||||
|
calledOnce := false
|
||||||
|
called := make(chan bool)
|
||||||
|
|
||||||
|
conn, err := NewConnection("amqp://guest:guest@broker:5672/", "event_PublishConsumeer", "webhooks")
|
||||||
|
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
// Clean all messages if any...
|
||||||
|
conn.channel.QueuePurge(conn.queueName, false)
|
||||||
|
|
||||||
|
c := NewConsumer(conn, false)
|
||||||
|
|
||||||
|
c.Subscribe("my_action", func(body []byte) bool {
|
||||||
|
if calledOnce {
|
||||||
|
called <- true
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
calledOnce = true
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
c.Listen()
|
||||||
|
|
||||||
|
p := NewProducer(conn)
|
||||||
|
|
||||||
|
err = p.Publish("my_action", []byte(""))
|
||||||
|
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-called:
|
||||||
|
case <-time.After(5 * time.Second):
|
||||||
|
assert.Fail(t, "timed out")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPublishConsumeRequeueIfPanic(t *testing.T) {
|
||||||
|
calledOnce := false
|
||||||
|
called := make(chan bool)
|
||||||
|
|
||||||
|
conn, err := NewConnection("amqp://guest:guest@broker:5672/", "event_PublishConsumeer", "webhooks")
|
||||||
|
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
// Clean all messages if any...
|
||||||
|
conn.channel.QueuePurge(conn.queueName, false)
|
||||||
|
|
||||||
|
c := NewConsumer(conn, false)
|
||||||
|
|
||||||
|
c.Subscribe("my_action", func(body []byte) bool {
|
||||||
|
if calledOnce {
|
||||||
|
called <- true
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
calledOnce = true
|
||||||
|
panic("this is a panic!")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
c.Listen()
|
||||||
|
|
||||||
|
p := NewProducer(conn)
|
||||||
|
|
||||||
|
err = p.Publish("my_action", []byte(""))
|
||||||
|
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-called:
|
||||||
|
case <-time.After(5 * time.Second):
|
||||||
|
assert.Fail(t, "timed out")
|
||||||
|
}
|
||||||
|
}
|
27
producer.go
Normal file
27
producer.go
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
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)
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user