├── .gitignore ├── LICENCE ├── README.md ├── producer.go ├── consumer.go └── amqpc.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2013 GoCardless Ltd. 2 | 3 | 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: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | amqpc 2 | ===== 3 | 4 | amqpc is a tool to test your AMQP broker. 5 | It has been developed and tested with RabbitMQ but any AMQP broker should work. 6 | We put it together very quickly to benchmark and test our RabbitMQ HA Cluster, so it might be unstable. 7 | 8 | ## Usage 9 | 10 | ```bash 11 | $ amqpc -h 12 | 13 | amqpc is CLI tool for testing AMQP brokers 14 | Usage: 15 | Consumer : amqpc [options] -c exchange routingkey queue 16 | Producer : amqpc [options] -p exchange routingkey [message] 17 | 18 | Options: 19 | -c=true: Act as a consumer 20 | -ct="amqpc-consumer": AMQP consumer tag (should not be blank) 21 | -g=1: Concurrency 22 | -i=500: (Producer only) Interval at which send messages (in ms) 23 | -n=0: (Producer only) Number of messages to send 24 | -p=false: Act as a producer 25 | -r=true: Wait for the publisher confirmation before exiting 26 | -t="direct": Exchange type - direct|fanout|topic|x-custom 27 | -u="amqp://guest:guest@localhost:5672/": AMQP URI 28 | ``` 29 | 30 | ## Installation 31 | 32 | Right now it's not available in any package manager. 33 | To build it your need to have a Go compiler. 34 | 35 | ```bash 36 | $ go get 37 | $ go build 38 | ``` 39 | 40 | ## Examples 41 | 42 | ```bash 43 | $ # Start one consumer 44 | $ amqpc -u amqp://guest:guest@localhost:5672/ -c your-exchange routing-key your-queue 45 | $ # Start one producer 46 | $ amqpc -u amqp://guest:guest@localhost:5672/ -p your-exchange routing-key your-message 47 | $ # Start 10 producers 48 | $ amqpc -u amqp://guest:guest@localhost:5672/ -g 10 -p your-exchange routing-key your-message 49 | $ # Start 10 producers, each one will be sending 100 at a rate of 1msg/s 50 | $ amqpc -u amqp://guest:guest@localhost:5672/ -g 10 -i 1000 -n 100 -p your-exchange routing-key your-message 51 | ``` 52 | 53 | ## TODO 54 | 55 | * Package management 56 | * Tests 57 | * Publisher confirms 58 | -------------------------------------------------------------------------------- /producer.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/streadway/amqp" 6 | "log" 7 | ) 8 | 9 | type Producer struct { 10 | connection *amqp.Connection 11 | channel *amqp.Channel 12 | tag string 13 | done chan error 14 | } 15 | 16 | func NewProducer(amqpURI, exchange, exchangeType, key, ctag string, reliable bool) (*Producer, error) { 17 | p := &Producer{ 18 | connection: nil, 19 | channel: nil, 20 | tag: ctag, 21 | done: make(chan error), 22 | } 23 | 24 | var err error 25 | 26 | log.Printf("Connecting to %s", amqpURI) 27 | p.connection, err = amqp.Dial(amqpURI) 28 | if err != nil { 29 | return nil, fmt.Errorf("Dial: ", err) 30 | } 31 | 32 | log.Printf("Getting Channel ") 33 | p.channel, err = p.connection.Channel() 34 | if err != nil { 35 | return nil, fmt.Errorf("Channel: ", err) 36 | } 37 | 38 | log.Printf("Declaring Exchange (%s)", exchange) 39 | if err := p.channel.ExchangeDeclare( 40 | exchange, // name 41 | exchangeType, // type 42 | true, // durable 43 | false, // auto-deleted 44 | false, // internal 45 | false, // noWait 46 | nil, // arguments 47 | ); err != nil { 48 | return nil, fmt.Errorf("Exchange Declare: %s", err) 49 | } 50 | 51 | // Reliable publisher confirms require confirm.select support from the 52 | // connection. 53 | // if reliable { 54 | // if err := p.channel.Confirm(false); err != nil { 55 | // return nil, fmt.Errorf("Channel could not be put into confirm mode: ", err) 56 | // } 57 | 58 | // ack, nack := p.channel.NotifyConfirm(make(chan uint64, 1), make(chan uint64, 1)) 59 | 60 | // // defer confirmOne(ack, nack) 61 | // } 62 | 63 | return p, nil 64 | } 65 | 66 | func (p *Producer) Publish(exchange, routingKey, body string) error { 67 | log.Printf("Publishing %s (%dB)", body, len(body)) 68 | 69 | if err := p.channel.Publish( 70 | exchange, // publish to an exchange 71 | routingKey, // routing to 0 or more queues 72 | false, // mandatory 73 | false, // immediate 74 | amqp.Publishing{ 75 | Headers: amqp.Table{}, 76 | ContentType: "text/plain", 77 | ContentEncoding: "", 78 | Body: []byte(body), 79 | DeliveryMode: amqp.Transient, // 1=non-persistent, 2=persistent 80 | Priority: 0, // 0-9 81 | // a bunch of application/implementation-specific fields 82 | }, 83 | ); err != nil { 84 | return fmt.Errorf("Exchange Publish: ", err) 85 | } 86 | 87 | return nil 88 | } 89 | 90 | // One would typically keep a channel of publishings, a sequence number, and a 91 | // set of unacknowledged sequence numbers and loop until the publishing channel 92 | // is closed. 93 | func confirmOne(ack, nack chan uint64) { 94 | log.Printf("waiting for confirmation of one publishing") 95 | 96 | select { 97 | case tag := <-ack: 98 | log.Printf("confirmed delivery with delivery tag: %d", tag) 99 | case tag := <-nack: 100 | log.Printf("failed delivery of delivery tag: %d", tag) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /consumer.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/streadway/amqp" 6 | "log" 7 | ) 8 | 9 | type Consumer struct { 10 | connection *amqp.Connection 11 | channel *amqp.Channel 12 | tag string 13 | done chan error 14 | } 15 | 16 | func NewConsumer(amqpURI, exchange, exchangeType, queue, key, ctag string) (*Consumer, error) { 17 | c := &Consumer{ 18 | connection: nil, 19 | channel: nil, 20 | tag: ctag, 21 | done: make(chan error), 22 | } 23 | 24 | var err error 25 | 26 | log.Printf("Connecting to %s", amqpURI) 27 | c.connection, err = amqp.Dial(amqpURI) 28 | if err != nil { 29 | return nil, fmt.Errorf("Dial: %s", err) 30 | } 31 | 32 | log.Printf("Getting Channel") 33 | c.channel, err = c.connection.Channel() 34 | if err != nil { 35 | return nil, fmt.Errorf("hannel: %s", err) 36 | } 37 | 38 | log.Printf("Declaring Exchange (%s)", exchange) 39 | if err = c.channel.ExchangeDeclare( 40 | exchange, // name of the exchange 41 | exchangeType, // type 42 | true, // durable 43 | false, // delete when complete 44 | false, // internal 45 | false, // noWait 46 | nil, // arguments 47 | ); err != nil { 48 | return nil, fmt.Errorf("Exchange Declare: %s", err) 49 | } 50 | 51 | log.Printf("Declaring Queue (%s)", queue) 52 | state, err := c.channel.QueueDeclare( 53 | queue, // name of the queue 54 | true, // durable 55 | false, // delete when usused 56 | false, // exclusive 57 | false, // noWait 58 | nil, // arguments 59 | ) 60 | if err != nil { 61 | return nil, fmt.Errorf("Queue Declare: %s", err) 62 | } 63 | 64 | log.Printf("Declared Queue (%d messages, %d consumers), binding to Exchange (key '%s')", state.Messages, state.Consumers, key) 65 | if err = c.channel.QueueBind( 66 | queue, // name of the queue 67 | key, // routingKey 68 | exchange, // sourceExchange 69 | false, // noWait 70 | nil, // arguments 71 | ); err != nil { 72 | return nil, fmt.Errorf("Queue Bind: %s", err) 73 | } 74 | 75 | log.Printf("Queue bound to Exchange, starting Consume (consumer tag '%s')", c.tag) 76 | deliveries, err := c.channel.Consume( 77 | queue, // name 78 | c.tag, // consumerTag, 79 | true, // autoAck 80 | false, // exclusive 81 | false, // noLocal 82 | false, // noWait 83 | nil, // arguments 84 | ) 85 | if err != nil { 86 | return nil, fmt.Errorf("Queue Consume: %s", err) 87 | } 88 | 89 | go handle(deliveries, c.done) 90 | 91 | return c, nil 92 | } 93 | 94 | func (c *Consumer) Shutdown() error { 95 | // will close() the deliveries channel 96 | if err := c.channel.Cancel(c.tag, true); err != nil { 97 | return fmt.Errorf("Consumer cancel failed: %s", err) 98 | } 99 | 100 | if err := c.connection.Close(); err != nil { 101 | return fmt.Errorf("AMQP connection close error: %s", err) 102 | } 103 | 104 | defer log.Printf("AMQP shutdown OK") 105 | 106 | // wait for handle() to exit 107 | return <-c.done 108 | } 109 | 110 | func handle(deliveries <-chan amqp.Delivery, done chan error) { 111 | for d := range deliveries { 112 | log.Printf( 113 | "Got %dB delivery: [%v] %s", 114 | len(d.Body), 115 | d.DeliveryTag, 116 | d.Body, 117 | ) 118 | } 119 | log.Printf("Handle: deliveries channel closed") 120 | done <- nil 121 | } 122 | -------------------------------------------------------------------------------- /amqpc.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/sha1" 5 | "encoding/base64" 6 | "flag" 7 | "fmt" 8 | "log" 9 | "os" 10 | "time" 11 | ) 12 | 13 | const ( 14 | DEFAULT_EXCHANGE string = " " 15 | DEFAULT_EXCHANGE_TYPE string = "direct" 16 | DEFAULT_QUEUE string = "amqpc-queue" 17 | DEFAULT_ROUTING_KEY string = "amqpc-key" 18 | DEFAULT_CONSUMER_TAG string = "amqpc-consumer" 19 | DEFAULT_RELIABLE bool = true 20 | DEFAULT_INTERVAL int = 500 21 | DEFAULT_MESSAGE_COUNT int = 0 22 | DEFAULT_CONCURRENCY int = 1 23 | ) 24 | 25 | var ( 26 | exchange *string 27 | routingKey *string 28 | queue *string 29 | body *string 30 | ) 31 | 32 | // Flags 33 | var ( 34 | consumer = flag.Bool("c", true, "Act as a consumer") 35 | producer = flag.Bool("p", false, "Act as a producer") 36 | 37 | // RabbitMQ related 38 | uri = flag.String("u", "amqp://guest:guest@localhost:5672/", "AMQP URI") 39 | exchangeType = flag.String("t", DEFAULT_EXCHANGE_TYPE, "Exchange type - direct|fanout|topic|x-custom") 40 | consumerTag = flag.String("ct", DEFAULT_CONSUMER_TAG, "AMQP consumer tag (should not be blank)") 41 | reliable = flag.Bool("r", DEFAULT_RELIABLE, "Wait for the publisher confirmation before exiting") 42 | 43 | // Test bench related 44 | concurrency = flag.Int("g", DEFAULT_CONCURRENCY, "Concurrency") 45 | interval = flag.Int("i", DEFAULT_INTERVAL, "(Producer only) Interval at which send messages (in ms)") 46 | messageCount = flag.Int("n", DEFAULT_MESSAGE_COUNT, "(Producer only) Number of messages to send") 47 | ) 48 | 49 | func usage() { 50 | fmt.Fprintf(os.Stderr, "amqpc is CLI tool for testing AMQP brokers\n") 51 | fmt.Fprintf(os.Stderr, "Usage:\n") 52 | fmt.Fprintf(os.Stderr, " Consumer : amqpc [options] -c exchange routingkey queue\n") 53 | fmt.Fprintf(os.Stderr, " Producer : amqpc [options] -p exchange routingkey [message]\n") 54 | fmt.Fprintf(os.Stderr, "\nOptions:\n") 55 | flag.PrintDefaults() 56 | os.Exit(1) 57 | } 58 | 59 | func init() { 60 | flag.Usage = usage 61 | flag.Parse() 62 | } 63 | 64 | func main() { 65 | done := make(chan error) 66 | 67 | flag.Usage = usage 68 | args := flag.Args() 69 | 70 | if flag.NArg() != 3 { 71 | log.Printf("You're missing arguments") 72 | os.Exit(1) 73 | } 74 | 75 | exchange = &args[0] 76 | routingKey = &args[1] 77 | 78 | if *producer { 79 | body = &args[2] 80 | for i := 0; i < *concurrency; i++ { 81 | go startProducer(done, body, *messageCount, *interval) 82 | } 83 | } else { 84 | queue = &args[2] 85 | for i := 0; i < *concurrency; i++ { 86 | go startConsumer(done) 87 | } 88 | } 89 | 90 | err := <-done 91 | if err != nil { 92 | log.Fatalf("Error : %s", err) 93 | } 94 | 95 | log.Printf("Exiting...") 96 | } 97 | 98 | func startConsumer(done chan error) { 99 | _, err := NewConsumer( 100 | *uri, 101 | *exchange, 102 | *exchangeType, 103 | *queue, 104 | *routingKey, 105 | *consumerTag, 106 | ) 107 | 108 | if err != nil { 109 | log.Fatalf("Error while starting consumer : %s", err) 110 | } 111 | 112 | <-done 113 | } 114 | 115 | func startProducer(done chan error, body *string, messageCount, interval int) { 116 | p, err := NewProducer( 117 | *uri, 118 | *exchange, 119 | *exchangeType, 120 | *routingKey, 121 | *consumerTag, 122 | true, 123 | ) 124 | 125 | if err != nil { 126 | log.Fatalf("Error while starting producer : %s", err) 127 | } 128 | 129 | var i int = 1 130 | for { 131 | publish(p, body, i) 132 | 133 | i++ 134 | if messageCount != 0 && i > messageCount { 135 | break 136 | } 137 | 138 | time.Sleep(time.Duration(interval) * time.Millisecond) 139 | } 140 | 141 | done <- nil 142 | } 143 | 144 | func publish(p *Producer, body *string, i int) { 145 | // Generate SHA for body 146 | hasher := sha1.New() 147 | hasher.Write([]byte(*body + string(i))) 148 | sha := base64.URLEncoding.EncodeToString(hasher.Sum(nil)) 149 | 150 | // String to publish 151 | bodyString := fmt.Sprintf("body: %s - hash: %s", *body, sha) 152 | 153 | p.Publish(*exchange, *routingKey, bodyString) 154 | } 155 | --------------------------------------------------------------------------------